1. 程式人生 > >記一次在BroadcastReceiver或Service裏彈窗的“完美”實踐

記一次在BroadcastReceiver或Service裏彈窗的“完美”實踐

.net 電源 屬性 amp nsa troy 界面 lag turn

  事情是這樣的,目前在做一個醫療項目,需要定時在某個時間段比如午休時間和晚上讓我們的App休眠,那麽這個時候在休眠時間段如果用戶按了電源鍵點亮屏幕了,我們就需要彈出一個全屏的窗口去做一個人性化的提示,“當前時間是休眠時間,請稍安勿躁…blabla”這樣子。

  很顯然,我們需要一個BroadcastReceiver來監聽系統的鎖屏,亮屏,用戶的解鎖,息屏行為,在收到亮屏廣播的時候彈窗。那麽如果是你,會選擇怎麽樣的方式去實現呢?

  兩種方案:

  • Dialog彈窗,全屏
  • 啟動一個Activity   

一. Dialog

這裏省去我們項目裏面的代碼,以簡單常用的AlertDialog為例

正常彈出AlertDialog的流程如下:

1 new AlertDialog.Builder(context).setTitle("在BroadcastReceiver裏彈出AlertDialog").show();

但是其實Dialog似乎只能在activity中彈出,至於為什麽,網上已經有很多相關文章了。這裏我隨手用百度Google了兩篇:

  • 為什麽Dialog不能用Application的Context
  • 創建Dialog所需的上下文為什麽必須是Activity?

為了解決在BroadcastReceiver裏彈出AlertDialog這個問題,我們可以這樣做:

  • 方案一

    將Dialog的窗口類型設置為TYPE_SYSTEM_ALERT

1 2 3 AlertDialog alertDialog=new AlertDialog.Builder(context).create(); alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); alertDialog.show();

需要註意的是,最後還要在androidManifest.xml文件中加入以下兩句話:

1 2 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW"/>

事實上,如果你認真看了我給出的度娘到的兩篇文章,你會發現這並不是一個很好的方案。

  • 方案二

    自定義Activity管理者或者說容器吧,通過它來獲取當前界面的Activity作為Dialog的context

    技術分享

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class MyActivityManager { private static MyActivityManager sInstance = new MyActivityManager(); private WeakReference<Activity> sCurrentActivityWeakRef; private List<Activity> activityList = new LinkedList<Activity>(); private MyActivityManager() { } public synchronized static MyActivityManager getInstance() { return sInstance; } public Activity getCurrentActivity() { Activity currentActivity = null; if (sCurrentActivityWeakRef != null) { currentActivity = sCurrentActivityWeakRef.get(); } return currentActivity; } public void setCurrentActivity(Activity activity) { sCurrentActivityWeakRef = new WeakReference<>(activity); } // add Activity public void addActivity(Activity activity) { if (!activityList.contains(activity)) activityList.add(activity); } // remove Activity public void removeActivity(Activity activity) { if (activityList.contains(activity)) activityList.remove(activity); } public void exitToHome() { try { for (Activity activity:activityList) { if (activity != null) { String className = activity.getClass().getSimpleName(); if (!className.equals("HomeActivity")) activity.finish(); } } } catch (Exception e) { e.printStackTrace(); } finally { } } //關閉每一個list內的activity public void finishActivityList() { for (Activity activity : activityList) { activity.finish(); } } }

    在你的application裏面

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { MyActivityManager.getInstance().addActivity(activity); } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { MyActivityManager.getInstance().setCurrentActivity(activity); } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { MyActivityManager.getInstance().removeActivity(activity); } });

如寫的鄙陋還請見諒, 當然了類似的工具類在網上也有很多。這裏順便再提一下

給dialog設置全屏的最簡單的方法 ,在構造函數中
super(context,android.R.style.Theme);
setOwnerActivity((Activity)context);
如果該Dialog設置了自定義style,則在其初始化完view後,設置layout寬高
getWindow().setLayout(屏幕寬,屏幕高);

二. Activity

直接上代碼:

1 2 3 Intent intent=new Intent(context,AnotherActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);

註意一定要給Intent設置一個flag:FLAG_ACTIVITY_NEW_TASK,不寫的話會拋異常:

* 可捕獲異常信息:
 * android.util.AndroidRuntimeException: 
 * Calling startActivity() from outside of an Activity context     requires the FLAG_ACTIVITY_NEW_TASK flag. 
 * Is this really what you want?

Why ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 * 1 在普通情況下,必須要有前一個Activity的Context,才能啟動後一個Activity * 2 但是在BroadcastReceiver裏面是沒有Activity的Context的 * 3 對於startActivity()方法,源碼中有這麽一段描述: * Note that if this method is being called from outside of an * {@link android.app.Activity} Context, then the Intent must include * the {@link Intent#FLAG_ACTIVITY_NEW_TASK} launch flag. This is because, * without being started from an existing Activity, there is no existing * task in which to place the new activity and thus it needs to be placed * in its own separate task. * 說白了就是如果不加這個flag就沒有一個Task來存放新啟動的Activity. * * 4 其實該flag和設置Activity的LaunchMode為SingleTask的效果是一樣的 * * * 如有更加深入的理解,請指點,多謝^_^

最後

我在項目裏采用的是啟動Activity的方法,just for easy ,比較符合需求場景,不用考慮全屏,Activity只做提示作用 基本沒有什麽代碼

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class DormancyReminderActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_dormancy_reminder) EventBus.getDefault().register(this) time.text = intent.getStringExtra("reminder") @Subscribe fun onScreenOnEvent(event: ScreenOnEvent) { Logger.d("get onScreenOnEvent") finish() } override fun onDestroy() { super.onDestroy() EventBus.getDefault().unregister(this) } override fun onBackPressed() { } }

屏蔽返回鍵事件,EventBus註冊接收到亮屏事件,在亮屏時finish,沒啥好說的。值得註意的是考慮到在休眠的時候,用戶按電源鍵 解鎖,息屏的時候,會不斷創建Activity加入到棧中,所以要在AndroidManifest文件中給Activity的啟動模式設為singleInstance

1 2 3 <activity android:name="com.hykd.model.compate.DormancyReminderActivity" android:launchMode="singleInstance"/>

鑒於我是一個Android萌新,這裏又要回顧一下Activity的四種啟動模式了,大神請略過^_^
容我簡單說一下它們的使用場景:
>

Activity啟動方式有四種,分別是:

  • standard
  • singleTop
  • singleTask
  • singleInstance

可以根據實際的需求為Activity設置對應的啟動模式,從而可以避免創建大量重復的Activity等問題。

設置Activity的啟動模式,只需要在AndroidManifest.xml裏對應的標簽設置android:launchMode屬性,例如:

下面是這四種模式的作用:

  • standard
  • 默認模式,可以不用寫配置。在這個模式下,都會默認創建一個新的實例。因此,在這種模式下,可以有多個相同的實例,也允許多個相同Activity疊加。

例如:
若我有一個Activity名為A1, 上面有一個按鈕可跳轉到A1。那麽如果我點擊按鈕,便會新啟一個Activity A1疊在剛才的A1之上,再點擊,又會再新啟一個在它之上……
點back鍵會依照棧順序依次退出。

  • singleTop
  • 可以有多個實例,但是不允許多個相同Activity疊加。即,如果Activity在棧頂的時候,啟動相同的Activity,不會創建新的實例,而會調用其onNewIntent方法。

例如:
若我有兩個Activity名為B1,B2,兩個Activity內容功能完全相同,都有兩個按鈕可以跳到B1或者B2,唯一不同的是B1為standard,B2為singleTop。
若我意圖打開的順序為B1->B2->B2,則實際打開的順序為B1->B2(後一次意圖打開B2,實際只調用了前一個的onNewIntent方法)
若我意圖打開的順序為B1->B2->B1->B2,則實際打開的順序與意圖的一致,為B1->B2->B1->B2。

  • singleTask
  • 只有一個實例。在同一個應用程序中啟動他的時候,若Activity不存在,則會在當前task創建一個新的實例,若存在,則會把task中在其之上的其它Activity destory掉並調用它的onNewIntent方法。
    如果是在別的應用程序中啟動它,則會新建一個task,並在該task中啟動這個Activity,singleTask允許別的Activity與其在一個task中共存,也就是說,如果我在這個singleTask的實例中再打開新的Activity,這個新的Activity還是會在singleTask的實例的task中。

例如:
若我的應用程序中有三個Activity,C1,C2,C3,三個Activity可互相啟動,其中C2為singleTask模式,那麽,無論我在這個程序中如何點擊啟動,如:C1->C2->C3->C2->C3->C1-C2,C1,C3可能存在多個實例,但是C2只會存在一個,並且這三個Activity都在同一個task裏面。
但是C1->C2->C3->C2->C3->C1-C2,這樣的操作過程實際應該是如下這樣的,因為singleTask會把task中在其之上的其它Activity destory掉。
操作:C1->C2 C1->C2->C3 C1->C2->C3->C2 C1->C2->C3->C2->C3->C1 C1->C2->C3->C2->C3->C1-C2
實際:C1->C2 C1->C2->C3 C1->C2 C1->C2->C3->C1 C1->C2

若是別的應用程序打開C2,則會新啟一個task。
如別的應用Other中有一個activity,taskId為200,從它打開C2,則C2的taskIdI不會為200,例如C2的taskId為201,那麽再從C2打開C1、C3,則C2、C3的taskId仍為201。
註意:如果此時你點擊home,然後再打開Other,發現這時顯示的肯定會是Other應用中的內容,而不會是我們應用中的C1 C2 C3中的其中一個。

  • singleInstance
  • 只有一個實例,並且這個實例獨立運行在一個task中,這個task只有這個實例,不允許有別的Activity存在。

例如:
程序有三個ActivityD1,D2,D3,三個Activity可互相啟動,其中D2為singleInstance模式。那麽程序從D1開始運行,假設D1的taskId為200,那麽從D1啟動D2時,D2會新啟動一個task,即D2與D1不在一個task中運行。假設D2的taskId為201,再從D2啟動D3時,D3的taskId為200,也就是說它被壓到了D1啟動的任務棧中。

若是在別的應用程序打開D2,假設Other的taskId為200,打開D2,D2會新建一個task運行,假設它的taskId為201,那麽如果這時再從D2啟動D1或者D3,則又會再創建一個task,因此,若操作步驟為other->D2->D1,這過程就涉及到了3個task了。

插曲

至此本次需求就已經完美實現了,細心的你可能發現了我的標題完美是打引號的,那麽又有怎樣的插曲呢 哎??

因為今天是我學習kotlin的第一天,也是第一次嘗試,當我加載Activity界面的時候,打出onCreate隨手回車,系統自動給我提供了這麽一個onCreate():

1 2 3 override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { super.onCreate(savedInstanceState, persistentState) }

Java代碼:

1 2 3 4 @Override public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) { super.onCreate(savedInstanceState, persistentState); }

然而我這小白並沒有發現,導致我的休眠提醒界面,setContentView之後卻始終顯示一片白,找遍一切可能出錯的地方,屬實浪費不少時間,最後在這個onCreate方法上面發現了貓膩(在這個onCreate方法裏寫了一個輸出,發現根本沒走這個方法!!!)。

第一反應,我並不認識這是一個什麽玩意。打開陳舊的api文檔,也沒有發現PersistableBundle這個類,於是只能求助百度,Google。原來是Api21新加的特性,上一下google,找一下最新api。我們先來看一下PersistableBundle是什麽東西。

A mapping from String values to various types that can be saved to persistent and later restored.

  顯然,這是一個和Bundle差不多的東西,Bundle我們就比較熟悉了。他兩都是一個鍵值對,前者多了這麽一段話,can be saved to persistent and later restored,可以持久化保存並且可以恢復。我們再看一下新的onCreate()方法的源碼。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * Same as {@link #onCreate(android.os.Bundle)} but called for those activities created with * the attribute {@link android.R.attr#persistableMode} set to * <code>persistAcrossReboots</code>. * * @param savedInstanceState if the activity is being re-initialized after * previously being shut down then this Bundle contains the data it most * recently supplied in {@link #onSaveInstanceState}. * <b><i>Note: Otherwise it is null.</i></b> * @param persistentState if the activity is being re-initialized after * previously being shut down or powered off then this Bundle contains the data it most * recently supplied to outPersistentState in {@link #onSaveInstanceState}. * <b><i>Note: Otherwise it is null.</i></b> public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { onCreate(savedInstanceState); }

  從源碼中可以看到,依然是調用了原始的onCreate()方法,結合以下兩個方法,

1 2 3 4 5 6 7 8 9 @Override public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { super.onSaveInstanceState(outState, outPersistentState); } @Override public void onRestoreInstanceState(Bundle savedInstanceState, PersistableBundle persistentState) { super.onRestoreInstanceState(savedInstanceState, persistentState); }

  最後記得在配置文件中註冊當前Activity的時候加上這個屬性,android:persistableMode=”persistAcrossReboots”,這樣就可以給你的Activity存儲一些持久化數據。當你的手機重啟或者發生其他意外情況的時候,也可以給你的頁面獲取到相關數據。

結尾

再次請求原諒我是一只Android萌新、小白,一個小小的需求實現啰嗦這麽多,打我別打臉^_^

記一次在BroadcastReceiver或Service裏彈窗的“完美”實踐