提示 2021年6月14日
這篇文章是 2019 年寫的规伐,今年我更新了一些新內(nèi)容,你可以直接看:《Android | Jetpack 處理回退事件的新姿勢(shì) —— OnBackPressedDispatcher》
前言
- “再按一次退出應(yīng)用”是一個(gè)常見的功能
- 但市面上流傳著太多不全面的“再按一次退出應(yīng)用”功能實(shí)現(xiàn)
- 本文將全面總結(jié)“再按一次退出應(yīng)用”的實(shí)現(xiàn)方式淌友,希望對(duì)你們有幫助
1. 需求分析
對(duì)比分析市面上App,可以分為四類磷账,歸納出完整流程圖:
- Facebook荣瑟、Instagram等:主Activity -> 主Tab -> 桌面
- 愛奇藝、高德地圖咏尝、招商等(最多):主Activity -> 兩次點(diǎn)擊 -> 結(jié)束Activity
- 微信压语、支付寶等:主Activity -> 返回桌面
- QQ音樂等:主Activity -> 兩次點(diǎn)擊 -> 返回桌面
-
需求本質(zhì)
- 判定連續(xù)按兩次返回鍵
- 結(jié)束Activity或者進(jìn)入后臺(tái)
2. 最小可行方案
-
需求:判定連續(xù)按兩次返回鍵后結(jié)束Activity
重寫Activity#onBackPressed()實(shí)現(xiàn)功能邏輯:
class MainActivity : AppCompatActivity() {
/**
* 上次點(diǎn)擊返回鍵的時(shí)間
*/
private var lastBackPressTime = -1L
...
override fun onBackPressed() {
val currentTIme = System.currentTimeMillis()
if(lastBackPressTime == -1L || currentTIme - lastBackPressTime >= 2000){
// 顯示提示信息
showBackPressTip()
// 記錄時(shí)間
lastBackPressTime = currentTIme
}else{
//退出應(yīng)用
finish()
}
}
private fun showBackPressTip(){
Toast.makeText(this,"再按一次退出",Toast.LENGTH_SHORT).show();
}
}
-
注意
因?yàn)镵ey事件會(huì)先傳遞給視圖中持有焦點(diǎn)的View,需要保證沒有View消費(fèi)KeyEvent.KEYCODE_BACK事件编检,否則Key事件無法傳遞到Activity中。
更多介紹參考:Android: Key事件分發(fā)機(jī)制
-
需求:進(jìn)入后臺(tái)
Activity#moveTaskToBack(boolean nonRoot)可以將當(dāng)前任務(wù)棧轉(zhuǎn)入后臺(tái)扰才,但不會(huì)真正結(jié)束Activity允懂。當(dāng)用戶返回應(yīng)用時(shí),不會(huì)重建Activity實(shí)例衩匣,Activity生命周期為:onStart() -> onResume()蕾总。
-
nonRoot參數(shù)
- true:只有當(dāng)前Activity處于棧底有效
- false:即使當(dāng)前Activity不處于棧底也有效
-
對(duì)比finish()
- finish()會(huì)結(jié)束Activity,moveTaskToBack()不會(huì)
- 用戶返回應(yīng)用時(shí)琅捏,使用finish()為溫啟動(dòng)生百,使用moveTaskToBack()為熱啟動(dòng),啟動(dòng)速度更快
-
對(duì)比System.exit(0)
- 直接殺死進(jìn)程柄延,但不推薦使用蚀浆。
- 要求兩次點(diǎn)擊返回鍵的目的是確保用戶是真的需要退出缀程,因此,退出后的行為應(yīng)該保持和沒有要求兩次點(diǎn)擊的效果一樣
- 默認(rèn)的返回鍵邏輯會(huì)結(jié)束Activity市俊,后續(xù)用戶返回應(yīng)用是溫啟動(dòng)杨凑,速度更快,而從System.exit(0)中恢復(fù)是冷啟動(dòng)摆昧,需要付出的代價(jià)大得多
-
更多介紹參考:Android: 任務(wù)棧與Activity啟動(dòng)模式
-
優(yōu)點(diǎn)
實(shí)現(xiàn)簡(jiǎn)單&方便
-
缺點(diǎn)
- 返回鍵無法收起可折疊的ActionBar
- 返回鍵無法彈出Fragment回退棧
-
適用場(chǎng)景
不需要通過返回鍵操作可折疊ActionBar和Fragment回退棧的情況撩满,如果能滿足需求,下文內(nèi)容均可以跳過绅你。
3. 源碼分析
由Key事件分發(fā)機(jī)制可知伺帘,如果沒有其他View消費(fèi)了返回鍵事件,最終將分發(fā)給Activity#onKeyUp():
// android.app.Activity:
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.ECLAIR) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
&& !event.isCanceled()) {
// 返回鍵事件
onBackPressed();
// 消費(fèi)KeyEvent.KEYCODE_BACK事件
return true;
}
}
// 不消費(fèi)
return false;
}
- 外部的條件判斷targetSdkVersion大于API 5忌锯,這是為了兼容舊時(shí)代的設(shè)備伪嫁,不要深究。
- 返回鍵事件分發(fā)最終分發(fā)到Activity#onBackPressed()
// android.app.Activity:
public void onBackPressed() {
// 收起可折疊ActionBar
if (mActionBar != null && mActionBar.collapseActionView()) {
return;
}
// 彈出Fragment回退棧棧頂?shù)腇ragment
FragmentManager fragmentManager = mFragments.getFragmentManager();
if (fragmentManager.isStateSaved()
|| !fragmentManager.popBackStackImmediate()) {
finishAfterTransition();
}
}
4. 演進(jìn)
-
支持android.support.v4.app.Fragment回退棧
按下返回鍵時(shí)優(yōu)先檢查Fragment回退棧并彈出:
// android.app.Activity:
override fun onBackPressed() {
// 彈出support庫Fragment回退棧
if(popSupportBackStack()){
return;
}
val currentTIme = System.currentTimeMillis();
...
}
/**
* @return true:沒有Fragment彈出 false:有Fragment彈出
*/
private fun popSupportBackStack():Boolean{
// 當(dāng)Fragment狀態(tài)保存了
return supportFragmentManager.isStateSaved
||supportFragmentManager.popBackStackImmediate()
}
- 注意
- 判斷supportFragmentManager.isStateSaved()非常有必要
- Activty#onSaveInstanceState()或者Activity#onStop()之后汉规,更改Fragment狀態(tài)礼殊,將拋出異常:
IllegalStateException:Can not perform this action after onSaveInstanceState
更多介紹參考:Android: Activity狀態(tài)管理
-
兼容性
重寫Activity#finishAfterTransition()可以保留源碼默認(rèn)的邏輯;
// android.app.Activity:
override fun finishAfterTransition() {
if(popSupportBackStack()){
return
}
val currentTIme = System.currentTimeMillis();
if(lastBackPressTime == -1L || currentTIme - lastBackPressTime >= 2000){
showBackPressTip()
lastBackPressTime = currentTIme
}else{
//退出應(yīng)用
finish()
}
}
- 支持WebView
使用WebView顯示多層級(jí)頁面针史,需要利用返回鍵向上導(dǎo)航:
class WebViewImpl(context: Context) : WebView(context) {
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
// 判斷可以向上導(dǎo)航
if(canGoBack()){
// 導(dǎo)航到之前的頁面
goBack()
return true
}
return false
}
}
5. 延伸閱讀
- Android | InputManagerService 與輸入事件采集
- [Android: Key事件分發(fā)機(jī)制]
- [Android: 任務(wù)棧與Activity啟動(dòng)模式]
- [Android: Activity狀態(tài)管理]
推薦閱讀
- Java | ThreadLocal 線程本地存儲(chǔ)
- Android | 自定義屬性
- Android | 文件存儲(chǔ)
- Android | InputManagerService與輸入事件采集
- 設(shè)計(jì)模式 | 靜態(tài)代理與動(dòng)態(tài)代理
- 筆記 | 使用 Keytool 管理密鑰和證書