記一次在廣播(BroadcastReceiver)或服務(Service)裏彈窗的「完美」實踐

事情是這樣的,目前在作一個醫療項目,須要定時在某個時間段好比午休時間和晚上讓咱們的App休眠,那麼這個時候在休眠時間段若是用戶按了電源鍵點亮屏幕了,咱們就須要彈出一個全屏的窗口去作一我的性化的提示,「當前時間是休眠時間,請稍安勿躁...blabla」這樣子。java

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


  兩種方案:api

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

一. Dialog

這裏省去咱們項目裏面的代碼,以簡單經常使用的AlertDialog爲例app

正常彈出AlertDialog的流程以下:

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

可是其實Dialog彷佛只能在activity中彈出,至於爲何,網上已經有不少相關文章了。這裏我隨手用百度Google了兩篇:ide

爲了解決在BroadcastReceiver裏彈出AlertDialog這個問題,咱們能夠這樣作:函數

  • 方案一

將Dialog的窗口類型設置爲TYPE_SYSTEM_ALERT工具

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

須要注意的是,最後還要在androidManifest.xml文件中加入如下兩句話:學習

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

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

  • 方案二

自定義Activity管理者或者說容器吧,經過它來獲取當前界面的Activity做爲Dialog的contextthis



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裏面

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

直接上代碼:

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 在普通狀況下,必需要有前一個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只作提示做用 基本沒有什麼代碼

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

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

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

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

  • standard
  • singleTop
  • singleTask
  • singleInstance

能夠根據實際的需求爲Activity設置對應的啓動模式,從而能夠避免建立大量重複的Activity等問題。

設置Activity的啓動模式,只須要在AndroidManifest.xml裏對應的<activity>標籤設置android:launchMode屬性,例如:
<activity
android:name=".A1"
android:launchMode="standard" />

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

  • 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打開C一、C3,則C二、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():

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

Java代碼:

@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()方法的源碼。

/** * 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()方法,結合如下兩個方法,

@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萌新、小白,一個小小的需求實現囉嗦這麼多,打我別打臉_

 

本文同步自個人我的小屋,歡迎來訪交流

相關文章
相關標籤/搜索