本文同步自我的個人小屋,歡迎來訪交流
事情是這樣的,目前在做一個醫療項目,需要定時在某個時間段比如午休時間和晚上讓我們的App休眠,那么這個時候在休眠時間段如果用戶按了電源鍵點亮屏幕了,我們就需要彈出一個全屏的窗口去做一個人性化的提示,“當前時間是休眠時間,請稍安勿躁...blabla”這樣子。
很顯然,我們需要一個BroadcastReceiver來監聽系統的鎖屏,亮屏,用戶的解鎖,息屏行為,在收到亮屏廣播的時候彈窗。那么如果是你,會選擇怎么樣的方式去實現呢?
兩種方案:
- Dialog彈窗,全屏
- 啟動一個Activity
一. Dialog
這里省去我們項目里面的代碼,以簡單常用的AlertDialog為例
正常彈出AlertDialog的流程如下:
new AlertDialog.Builder(context).setTitle("在BroadcastReceiver里彈出AlertDialog").show();
但是其實Dialog似乎只能在activity中彈出,至于為什么,網上已經有很多相關文章了。這里我隨手用百度Google了兩篇:
為了解決在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"/>
事實上,如果你認真看了我給出的度娘到的兩篇文章,你會發現這并不是一個很好的方案。
- 方案二
自定義Activity管理者或者說容器吧,通過它來獲取當前界面的Activity作為Dialog的context
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打開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():
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萌新、小白,一個小小的需求實現啰嗦這么多,打我別打臉_