編輯:關於Android編程
存在的問題
Android開發中不可避免的會遇到需要檢查app何時進入前台,何時被用戶關閉。奇怪的是,要達到這個目的並不容易。檢查app第一次啟動並不難,但要判斷它何時重新打開和關閉就沒有那麼簡單了。
這篇文章將介紹一種判斷app打開,重新打開和關閉的技術。
讓我們開始吧
判斷一個app打開和關閉的關鍵在於判斷它的activities是否正在前台顯示。讓我們先從簡單的例子開始,一個只有一個activity的app,而且不支持水平模式。這樣想要判斷app是打開還是關閉只需要檢查activity的onStart和onStop方法即可:
@Override
protected void onStart() {
super.onStart();
// The Application has been opened!
}
@Override
protected void onStop() {
super.onStop();
// The Application has been closed!
}
上面例子的問題在於當需要支持水平模式時該方法就失效了。當我們旋轉設備時activity將會重建,onStart方法將被再次調用,這時將會錯誤的判斷為app第二次被打開。
為了處理設備旋轉的情況,我們需要增加一個校驗步驟。當activity退出時啟動一個定時器,用於判斷短時間內app的這個activity是否又被啟動,如果沒有,說明用戶真的退出了這個app,如果重新啟動了這個activity,說明用戶還逗留在這個app中。
這種校驗方式也適用於擁有多個activities的app,因為從app的一個activity跳轉到另一個activity也可以用這種校驗方式來處理。
使用這個技術我創建了一個管理類,所有的activities在可見和不可見時都會通知這個管理類。這個管理類為每個activity處理上述的校驗步驟,從而避免錯誤的檢測。它也提供了發布訂閱(觀察者)模式,任何對app啟動和關閉感興趣的模塊都可以通過它來得到對應的通知。
把它添加到你的工程中
import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.text.format.DateUtils;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Set;
/**
* This class is responsible for tracking all currently open activities.
* By doing so this class can detect when the application is in the foreground
* and when it is running in the background.
*/
public class AppForegroundStateManager {
private static final String TAG = AppForegroundStateManager.class.getSimpleName();
private static final int MESSAGE_NOTIFY_LISTENERS = 1;
public static final long APP_CLOSED_VALIDATION_TIME_IN_MS = 30 * DateUtils.SECOND_IN_MILLIS; // 30 Seconds
private Reference mForegroundActivity;
private Set mListeners = new HashSet<>();
private AppForegroundState mAppForegroundState = AppForegroundState.NOT_IN_FOREGROUND;
private NotifyListenersHandler mHandler;
// Make this class a thread safe singleton
private static class SingletonHolder {
public static final AppForegroundStateManager INSTANCE = new AppForegroundStateManager();
}
public static AppForegroundStateManager getInstance() {
return SingletonHolder.INSTANCE;
}
private AppForegroundStateManager() {
// Create the handler on the main thread
mHandler = new NotifyListenersHandler(Looper.getMainLooper());
}
public enum AppForegroundState {
IN_FOREGROUND,
NOT_IN_FOREGROUND
}
public interface OnAppForegroundStateChangeListener {
/** Called when the foreground state of the app changes */
public void onAppForegroundStateChange(AppForegroundState newState);
}
/** An activity should call this when it becomes visible */
public void onActivityVisible(Activity activity) {
if (mForegroundActivity != null) mForegroundActivity.clear();
mForegroundActivity = new WeakReference<>(activity);
determineAppForegroundState();
}
/** An activity should call this when it is no longer visible */
public void onActivityNotVisible(Activity activity) {
/*
* The foreground activity may have been replaced with a new foreground activity in our app.
* So only clear the foregroundActivity if the new activity matches the foreground activity.
*/
if (mForegroundActivity != null) {
Activity ref = mForegroundActivity.get();
if (activity == ref) {
// This is the activity that is going away, clear the reference
mForegroundActivity.clear();
mForegroundActivity = null;
}
}
determineAppForegroundState();
}
/** Use to determine if this app is in the foreground */
public Boolean isAppInForeground() {
return mAppForegroundState == AppForegroundState.IN_FOREGROUND;
}
/**
* Call to determine the current state, update the tracking global, and notify subscribers if the state has changed.
*/
private void determineAppForegroundState() {
/* Get the current state */
AppForegroundState oldState = mAppForegroundState;
/* Determine what the new state should be */
final boolean isInForeground = mForegroundActivity != null && mForegroundActivity.get() != null;
mAppForegroundState = isInForeground ? AppForegroundState.IN_FOREGROUND : AppForegroundState.NOT_IN_FOREGROUND;
/* If the new state is different then the old state the notify subscribers of the state change */
if (mAppForegroundState != oldState) {
validateThenNotifyListeners();
}
}
/**
* Add a listener to be notified of app foreground state change events.
*
* @param listener
*/
public void addListener(@NonNull OnAppForegroundStateChangeListener listener) {
mListeners.add(listener);
}
/**
* Remove a listener from being notified of app foreground state change events.
*
* @param listener
*/
public void removeListener(OnAppForegroundStateChangeListener listener) {
mListeners.remove(listener);
}
/** Notify all listeners the app foreground state has changed */
private void notifyListeners(AppForegroundState newState) {
android.util.Log.i(TAG, "Notifying subscribers that app just entered state: " + newState);
for (OnAppForegroundStateChangeListener listener : mListeners) {
listener.onAppForegroundStateChange(newState);
}
}
/**
* This method will notify subscribes that the foreground state has changed when and if appropriate.
*
* We do not want to just notify listeners right away when the app enters of leaves the foreground. When changing orientations or opening and
* closing the app quickly we briefly pass through a NOT_IN_FOREGROUND state that must be ignored. To accomplish this a delayed message will be
* Sent when we detect a change. We will not notify that a foreground change happened until the delay time has been reached. If a second
* foreground change is detected during the delay period then the notification will be canceled.
*/
private void validateThenNotifyListeners() {
// If the app has any pending notifications then throw out the event as the state change has failed validation
if (mHandler.hasMessages(MESSAGE_NOTIFY_LISTENERS)) {
android.util.Log.v(TAG, "Validation Failed: Throwing out app foreground state change notification");
mHandler.removeMessages(MESSAGE_NOTIFY_LISTENERS);
} else {
if (mAppForegroundState == AppForegroundState.IN_FOREGROUND) {
// If the app entered the foreground then notify listeners right away; there is no validation time for this
mHandler.sendEmptyMessage(MESSAGE_NOTIFY_LISTENERS);
} else {
// We need to validate that the app entered the background. A delay is used to allow for time when the application went into the
// background but we do not want to consider the app being backgrounded such as for in app purchasing flow and full screen ads.
mHandler.sendEmptyMessageDelayed(MESSAGE_NOTIFY_LISTENERS, APP_CLOSED_VALIDATION_TIME_IN_MS);
}
}
}
private class NotifyListenersHandler extends Handler {
private NotifyListenersHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message inputMessage) {
switch (inputMessage.what) {
// The decoding is done
case MESSAGE_NOTIFY_LISTENERS:
/* Notify subscribers of the state change */
android.util.Log.v(TAG, "App just changed foreground state to: " + mAppForegroundState);
notifyListeners(mAppForegroundState);
break;
default:
super.handleMessage(inputMessage);
}
}
}
}
Activities在可見性改變的需要發送通知
app中所有activities都要增加下面的代碼,用於可見性改變時通知管理類。最好的實現方式是把這段代碼加到工程的BaseActivity中。
@Override
protected void onStart() {
super.onStart();
AppForegroundStateManager.getInstance().onActivityVisible(this);
}
@Override
protected void onStop() {
AppForegroundStateManager.getInstance().onActivityNotVisible(this);
super.onStop();
}
訂閱app的前台可見性改變事件
在感興趣的模塊中訂閱app前台可見性改變事件,application類的onCreate函數是一個不錯的地方,它可以保證每次app啟動和關閉,你都能得到通知。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
AppForegroundStateManager.getInstance().addListener(this);
}
@Override
public void onAppForegroundStateChange(AppForegroundStateManager.AppForegroundState newState) {
if (AppForegroundStateManager.AppForegroundState.IN_FOREGROUND == newState) {
// App just entered the foreground. Do something here!
} else {
// App just entered the background. Do something here!
}
}
}
進一步的思考
有一些細節需要進一步討論,下面討論的幾點針對具體的應用可以做微調。
校驗時間
校驗定時器檢查app是否真的進入後台的時間間隔是多少合適呢?上面的代碼設置為30秒,原因如下。
當你的app在運行時,可能存在第三方的activities會覆蓋全屏幕,一些常見的例子是Google應用內購買和Facebook登錄注冊頁面。這些情況下你的app都會被迫進入後台,前台用於顯示這些第三方頁面。如果把這種情況當做用戶離開了你的app,顯然是不對的。30秒超時設置就是用來避免這種情況的。例如當用戶在30秒內完成應用內購買,大部分用戶都可以做得到,那麼就不會當做用戶突然離開app了。
如果你的app不存在上述這種情況,我建議可以把你的校驗時間設置為4秒,這樣對於低配設備當屏幕旋轉重新創建activity的時間間隔是合適的。
CPU休眠
可能存在的問題是當用戶關閉app或者app仍處於前台時用戶鎖屏了,這時CPU可能不會等到定時器檢測就休眠了。為了保證這種情況下定時器能夠正常檢測用戶退出app,我們需要持有wakelock防止CPU休眠直到app關閉事件被確認。實踐中相比使用wakelock,這種情況並不算問題。
判斷app是如何啟動的
現在我們已經知道如何檢測app何時啟動和關閉,但我們不知道app是如何啟動的。是用戶點擊通知欄消息?還是點擊一個鏈接?亦或是他們直接通過桌面圖標或最近使用啟動?
跟蹤啟動機制
首先我們需要知道在哪裡檢測app是如何啟動的。基於前面一個例子我們可以打印出app何時啟動,以及如何啟動。
public class MyApplication extends Application {
public final String TAG = MyApplication.class.getSimpleName();
public enum LaunchMechanism {
DIRECT,
NOTIFICATION,
URL;
}
private LaunchMechanism mLaunchMechanism = LaunchMechanism.DIRECT;
public void setLaunchMechanism(LaunchMechanism launchMechanism) {
mLaunchMechanism = launchMechanism;
}
@Override
public void onCreate() {
super.onCreate();
AppForegroundStateManager.getInstance().addListener(this);
}
@Override
public void onAppForegroundStateChange(AppForegroundStateManager.AppForegroundState newState) {
if (AppForegroundStateManager.AppForegroundState.IN_FOREGROUND.equals(newState)) {
// App just entered the foreground.
Log.i(TAG, "App Just Entered the Foreground with launch mechanism of: " + mLaunchMechanism);
} else {
// App just entered the background. Set our launch mode back to the default of direct.
mLaunchMechanism = LaunchMechanism.DIRECT;
}
}
}
設置啟動機制
現在我們可以打印app何時啟動的機制,但我們沒有設置它。因此下一步就是在用戶通過鏈接或者通知啟動app時我們記下它。如果沒有通過這兩種方式設置過,說明用戶是通過點擊app圖標啟動的。
跟蹤鏈接點擊事件
為了跟蹤用戶點擊鏈接打開app,你需要找到代碼中處理鏈接的地方,並加入下面的代碼來跟蹤啟動機制。要確保這些代碼在activity的onStart()函數之前調用。在哪些地方加入下面的代碼取決於你的app架構了。
getApplication().setLaunchMechanism(LaunchMechanism.URL); 跟蹤通知事件
不幸的是跟蹤通知點擊需要更多技巧,通知顯示後,點擊它將會打開之前綁定好的一個PendingIntent,這裡的技巧是為通知的所有PendingIntents添加一個標識表明是由通知發出的。
例如當為通知創建PendingIntent時為每個intent添加如下代碼:
public static final String EXTRA_HANDLING_NOTIFICATION = "Notification.EXTRA_HANDLING_NOTIFICATION";
// Put an extra so we know when an activity launches if it is a from a notification
intent.putExtra(EXTRA_HANDLING_NOTIFICATION, true);
到這一步我們需要做的就是在每個activity(統一在BaseActivity中添加)中檢查這個標識。當識別到這個標識時說明是從通知啟動的,這時可以把啟動機制設置為通過通知。這一步應該在onCreate中處理,這樣在app啟動到前台之前就設置好了(會觸發啟動機制的打印)。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent != null && intent.getExtras() != null) {
// Detect if the activity was launched by the user clicking on a notification
if (intent.getExtras().getBoolean(EXTRA_HANDLING_NOTIFICATION, false)) {
// Notify that the activity was opened by the user clicking on a notification.
getApplication().setLaunchMechanism(LaunchMechanism.NOTIFICATION);
}
}
}
在Android3.0上開始引入了一個新概念叫Fragment。它有自己的布局文件,可以作為組件排布,也可以相互組合去實現不同的布局顯示。使用Fragment可以重復利用
首選項這個名詞對於熟悉Android的朋友們一定不會感到陌生,它經常用來設置軟件的運行參數。Android提供了一種健壯並且靈活的框架來處理首選項。它提供了簡單的API來
寫在前面一家移動互聯網公司,說到底,要盈利總是需要付費用戶的,自己開發支付系統顯然是不明智的,國內已經有多家成熟的移動支付提供商,騰訊就是其中之一。梳理了下微信支付的接入
為了讓用戶的使用更舒適所以有些情況使用動畫是很有必要的,Android在3.0以前支持倆種動畫Tween動畫以及Frame動畫。Tween動畫支持簡單的平移,縮放,旋轉,