close
安卓進階漲薪訓練營,讓一部分人先進大廠

大家好,我是皇叔,最近開了一個安卓進階漲薪訓練營,可以幫助大家突破技術&職場瓶頸,從而度過難關,進入心儀的公司。

詳情見文章:沒錯!皇叔開了個訓練營

Android 10 首次引入了全局返回手勢,但直到返回觸發才能看到目標上層畫面。13 針對該特性進行了優化,即返回觸發之前可以預覽上層畫面。同時徹底廢棄了返回鍵相關的 API,這將對現有的 App 邏輯產生巨大的影響!

前言

Android 13 針對包括手機、大屏、摺疊屏等 Android 設備推出了可預見型返回手勢(Predictive Back Gesture)特性。該特性將便於用戶在返回完成之前可以先預覽到目標畫面或結果,這樣的話可以允許他們決定是否要繼續返回或者放棄並停留在當前畫面。

另外引入關於KEYCODE_BACKKeyEvent 相關的一系列變更。

為節省篇幅和統一認識,後續的相關描述將按照如下規則簡稱:

本次引入的可預見型返回手勢 + KEYCODE_BACK 系列變更:統稱為新返回導航
KEYCODE_BACK KeyEvent:簡稱為 KEYCODE_BACK
傳統導航模式和 Swipe-Up 導航模式下的返回按鈕:簡稱為Back KeyButton
全局返回手勢:簡稱為Back Gesture
Back KeyButtonBack Gesture

後續將按照如下幾個方面去闡述:

新返回導航的具體影響
如何確定是否受影響
適配方案的選擇
適配方案的詳述
SDK API 適配方案的深入探討
新返回導航支持與否的深入比較和原理分析
注意和殘留事項
1. 新返回導航的具體影響

簡單來說會產生如下影響:

返回手勢的可預見型 UI 的增強:展示返回觸發前上層畫面
原有 API 廢棄:
KEYCODE_BACK:詳述見小章節
Activity/Dialog:onBackPressed()
引入全新的 SDK 返回相關 API:
Manifest 中enableOnBackInvokedCallback屬性
Activity/Dialog/Window:getOnBackInvokedDispatcher()
OnBackInvokedDispatcher
OnBackInvokedCallback

備註:無關TargetSDKVersion,運行在 13 上只要支持新返回導航均會受收到如上的影響。

KEYCODE_BACK 非推薦

準確含義是 13 上一旦開啟新返回導航支持,無論是 Back Gesture 的觸發還是 Back KeyButton 的點擊,App 均無法監聽到 KEYCODE_BACK 事件。即相關的如下 API 將無法被回調:

Activity:
dispatchKeyEvent()
onKeyDown()
onKeyUp()
onBackPressed()
Dialog:API 同上
2. 如何確定是否受影響

除了上述提到的具體變更以外,所有 KEYCODE_BACK 的相關邏輯都得測試一下是否存在問題,比如容易忽略的 View、Dialog$Builder。

簡單來說,檢查下現有代碼是否用到了如下 API:

Activity/Dialog#onBackPressed()
Activity:dispatchKeyEvent()、onKeyDown()、onKeyUp(),監聽 KEYCODE_BACK
Activity:使用 AndroidX 的 OnBackPressedDispatcher、OnBackPressedCallback API
Dialog:dispatchKeyEvent()、onKeyDown()、onKeyUp()、setOnKeyListener(),監聽 KEYCODE_BACK
AlertDialog$Builder:setOnKeyListener(),監聽 KEYCODE_BACK
View:dispatchKeyEvent()、onKeyDown()、onKeyUp()、setOnKeyListener(),監聽 KEYCODE_BACK
3. 適配方案的選擇

大多數 App 都會選擇自定義返回導航,可選的方式包括 SDK 的原生 API 和 AndroidX 的 Callback API。依據這些情況的不同、App 適配的意願不同,適配的方案也不一樣。

沒有自定義返回導航的場景

加入新返回導航的支持即可,具體見《4.1 加入新返回導航的支持》章節。

自定義返回導航的場景

需要按照現有 API 是否接入了 AndroidX 的 OnBackPressedDispatcher 進行分情況適配。

是否使用了AndroidX如何處理返回導航推薦的適配策略YesAndroidX APIs升級已有的 AndroidX 返回 APIUnsupported SDK APIs遷移非推薦 SDK 返回 API 到 AndroidX APINoUnsupported SDK APIs,但我願意遷移遷移非推薦 SDK 返回 API 到新 SDK 返回 APIUnsupported SDK APIs,但我不願意遷移延遲加入新返回導航的支持,直到它成為必須特性
4. 適配方案的詳述4.1 加入新返回導航的支持

Manifest 中針對新返回導航特性引入的屬性enableOnBackInvokedCallback默認是 false,即默認不支持該特性,支持的話需要聲明為 true。

<application...android:enableOnBackInvokedCallback="true"...>...</application>

實測發現:即便聲明成了 false,但如果代碼中殘存了 13 的新 API(比如 OnBackInvokedCallback)的使用,仍會導致新返回導航發生作用。

也就是說,不支持的話,就不要使用任何新的返回相關 API。

4.2 關閉新返回導航的支持

正如上面所述,按照如下即可關閉對新返回導航的支持:

enableOnBackInvokedCallback 聲明為 false(不聲明亦可)
不要使用 OnBackInvokedCallback 等返回相關 API
4.3 升級已有的 AndroidX 返回 API

對於已使用 AndroidX 返回 API 的 App 只需開啟新返回導航的支持,其他的適配工作交由 AndroidX 框架來完成。

Supporting the predictive back gesture requires updating your app, using theOnBackPressedCallbackAppCompat 1.6.0-alpha03(AndroidX) or higher API.

筆者按照官方說明將AppCompat包升級到了1.6.0-alpha03。

dependencies{implementation'androidx.appcompat:appcompat:1.6.0-alpha03'}

使用其提供的OnBackPressedCallbackAPI 監聽 Activity 的 Back 操作如下:

classBackKeyTestActivityAppCompat:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle?){...onBackPressedDispatcher.addCallback(object:OnBackPressedCallback(true){overridefunhandleOnBackPressed(){Log.d("BackGesture","Activity#handleOnBackPressed()")}})}overridefundispatchKeyEvent(event:KeyEvent):Boolean{Log.d("BackGesture","Activity#dispatchKeyEvent()event:$event")returnsuper.dispatchKeyEvent(event)}overridefunonKeyDown(keyCode:Int,event:KeyEvent):Boolean{Log.d("BackGesture","Activity#onKeyDown()event:$event")returnsuper.onKeyDown(keyCode,event)}overridefunonKeyUp(keyCode:Int,event:KeyEvent):Boolean{Log.d("BackGesture","Activity#onKeyUp()event:$event")returnsuper.onKeyUp(keyCode,event)}overridefunonBackPressed(){Log.d("BackGesture","onBackPressed()")super.onBackPressed()}}

可是實測發現:

即便在 13 上開啟了新返回導航,無論是 Back Gesture 還是 Back KeyButton,Callback 和 KeyEvent 回調均未執行,Activity 將直接結束
但同樣的代碼運行在 12 上的話,Back Gesture 和 Back KeyButton 下 Callback 和 KeyEvent 均能被回調

12-Back Gesture 的執行日誌:

05-3110:35:28.7321126711267DBackGesture:Activity#dispatchKeyEvent()event:KeyEvent{action=ACTION_DOWN,keyCode=KEYCODE_BACK,...}05-3110:35:28.7331126711267DBackGesture:Activity#onKeyDown()event:KeyEvent{action=ACTION_DOWN,keyCode=KEYCODE_BACK,...}05-3110:35:28.7331126711267DBackGesture:Activity#dispatchKeyEvent()event:KeyEvent{action=ACTION_UP,keyCode=KEYCODE_BACK,...}05-3110:35:28.7331126711267DBackGesture:Activity#onKeyUp()event:KeyEvent{action=ACTION_UP,keyCode=KEYCODE_BACK,...}05-3110:35:28.7331126711267DBackGesture:onBackPressed()05-3110:35:28.7341126711267DBackGesture:Activity#handleOnBackPressed()

12-Back KeyButton 的執行日誌:

05-3110:37:21.7241126711267DBackGesture:Activity#dispatchKeyEvent()event:KeyEvent{action=ACTION_DOWN,keyCode=KEYCODE_BACK...}05-3110:37:21.7241126711267DBackGesture:Activity#onKeyDown()event:KeyEvent{action=ACTION_DOWN,keyCode=KEYCODE_BACK...}05-3110:37:21.8461126711267DBackGesture:Activity#dispatchKeyEvent()event:KeyEvent{action=ACTION_UP,keyCode=KEYCODE_BACK...}05-3110:37:21.8461126711267DBackGesture:Activity#onKeyUp()event:KeyEvent{action=ACTION_UP,keyCode=KEYCODE_BACK...}05-3110:37:21.8461126711267DBackGesture:onBackPressed()05-3110:37:21.8461126711267DBackGesture:Activity#handleOnBackPressed()

調試了一下,發現 AppCompat 框架里使用 13 的新 SDK API 前的版本判斷有問題:

publicclassComponentActivity{protectedvoidonCreate(@NullableBundlesavedInstanceState){...if(Build.VERSION.SDK_INT>=33){mOnBackPressedDispatcher.setOnBackInvokedDispatcher(getOnBackInvokedDispatcher());}...}}publicfinalclassOnBackPressedDispatcher{CancellableaddCancellableCallback(@NonNullOnBackPressedCallbackonBackPressedCallback){...if(Build.VERSION.SDK_INT>=33){updateBackInvokedCallbackState();onBackPressedCallback.setIsEnabledConsumer(mEnabledConsumer);}returncancellable;}}

Beta版的SDK_INT常量仍然是12L的 32,到正式發布才會改為 33,所以版本判斷應當使用BuildCompat的如下 API:

//BuildCompat.javapublicstaticbooleanisAtLeastT(){returnVERSION.SDK_INT>=33||(VERSION.SDK_INT>=32&&isAtLeastPreReleaseCodename("Tiramisu",VERSION.CODENAME));}

官方文檔提示說的是使用 1.6.0-alpha03 及以上,那麼 03 應該是首次引入上述適配的版本,可能還沒做好。查了下 AppCompat 包是否出現最新版本,果然有個1.6.0-alpha04。

Version 1.6.0-alpha04

May 18, 2022

更新了後確實好了,即 13 上開啟支持的話,無論是 Back Gesture 還是 Back KeyButton,能像預期的那樣都只會輸出 androidX 版本的 Callback,Back 相關 KeyEvent 回調將不再執行。

05-3110:55:10.77350415041DBackGesture:Activity#handleOnBackPressed()

但仍有一點未達預期:

按理說 13 上關閉支持的話,無論是 Back Gesture 還是 Back KeyButton,運行結果應該和 12 保持一致,即收到 Back 相關 KeyEvent 回調以及 OnBackPressedCallback
可實測發現:只有 Back KeyButton 點擊是上述結果,Back Gesture 的話只收到了 Callback、沒有 KeyEvent 回調,這裡有點奇怪
4.4 遷移非推薦 SDK 返回 API 到 AndroidX API

適配步驟:

遷移已有的系統返回處理邏輯到 AndroidX 的OnBackPressedDispatcherAPI,他需要指定 OnBackPressedCallback 實現,詳細的可參考如何提供自定義返回導航

對於 Activity:

classMyActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)valcallback=onBackPressedDispatcher.addCallback(this){//Handlethebackbuttonevent}}...}

對於 Fragment:

publicclassFormEntryFragmentextendsFragment{@OverridepublicvoidonAttach(@NonNullContextcontext){super.onAttach(context);OnBackPressedCallbackcallback=newOnBackPressedCallback(true//defaulttoenabled){@OverridepublicvoidhandleOnBackPressed(){showAreYouSureDialog();}};requireActivity().getOnBackPressedDispatcher().addCallback(this,//LifecycleOwnercallback);}}

禁用原有的系統返回手勢回調,比如 onBackPressed()、KEYCODE_BACK

解釋:getOnBackPressedDispatcher 早在 13 之前就已經支持,既然換了就沒必要保留 SDK API 邏輯。

最後記得加入新返回導航的支持。

4.5 遷移非推薦 SDK 返回 API 到新 SDK 返回 API

適配步驟:

運行在 13 及之後的版本上使用全新的 SDK API 即OnBackInvokedCallback,12及之前的版本仍可使用舊的返回 API

在 Activity、Dialog、Window 等 Window 級別的組件里需要監聽返回手勢的邏輯處註冊實現了onBackInvoked方法的 OnBackInvokedCallback。這將阻止當前的 Activity 被結束,這樣的話當用戶觸發了系統返回操作的話你的 Callback 將有機會執行你預期的返回動作

為了確保正確支持系統「後退導航」的未來增強功能,你的 App 必須註銷 OnBackInvokedCallback。否則,用戶在使用系統後退導航時可能會看到不良行為,例如,在視圖之間「卡住」並強制他們退出應用。

To ensure that future enhancements to the system Back navigation are properly supported, your appMUSTunregister theOnBackInvokedCallback. Otherwise, users may see undesirable behavior when using a system Back navigation—for example, "getting stuck" between views and forcing them to force quit your app.

@OverridevoidonCreate(){if(BuildCompat.isAtLeastT()){getOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT,()->{//...});}}

比如 WebView 需要攔截返回手勢以回退網頁,當已經返回到主畫面的時候應當註銷該 Callback 讓系統來處理 finish。

同樣的,加入新返回導航的支持。

備註:onBackPressed() 邏輯保留也沒有關係,並不會發生衝突,而且為了兼容 13 之前的系統功能本就應該保留。

registerOnBackInvokedCallback() 說明

registerOnBackInvokedCallback() 調用的時候需要提供如下兩個參數:

priority:按照註冊的逆序進行,但如果是高優先級的先回調。可選範圍:int 型,亦可選如下預設常量:

但不可以是負值、否則會發生IllegalArgumentException異常

java.lang.IllegalArgumentException: Application registered OnBackInvokedCallback cannot have negative priority. Priority: -1

PRIORITY_DEFAULT:值為0,普通回調
PRIORITY_OVERLAY:值為1000000,優先回調

callback:OnBackInvokedCallback 實例,會在 Back Gesture 觸發、Back KeyButton 按壓的時候被回調

實際結果:只有最後一個 register 的 Callback 得到調用,但如果列表里存在 PRIORITY_OVERLAY 等更高優先級的 Callback 的話則優先。與如下描述不符:

When back is triggered, callbacks on the in-focus window are invoked in reverse order in which they are added within the same priority. Between different priorities, callbacks with higher priority are invoked first.

5. SDK API 適配方案的深入探討5.1 案例

和 KEYCODE_BACK 相關的有很多 API 可以處理、場景也很繁雜,簡單舉例如下:

覆寫 Activity#onKeyDown() 處理 KEYCODE_BACK 的 DOWN:
classActivity{overridefunonKeyDown(keyCode:Int,event:KeyEvent?):Boolean{if(...)returnfalsewhen(keyCode){KeyEvent.KEYCODE_BACK->{methodA()}KeyEvent.KEYCODE_MENU->{...}else->{}}returnif(...){true}elsesuper.onKeyDown(keyCode,event)}}

覆寫 Activity#onKeyUp() 處理 KEYCODE_BACK 的 UP

覆寫 Activity#dispatchKeyEvent() 將 KeyEvent 傳遞到 Fragment 處理

覆寫 Activity#onBackPressed() 處理返回回調

調用 Dialog#setOnKeyListener() 處理 KEYCODE_BACK

調用 AlertDialog.Builder#setOnKeyListener() 處理 KEYCODE_BACK

覆寫 Dialog#dispatchKeyEvent() 處理 KEYCODE_BACK

覆寫 EditText#onKeyPreIme() 處理 KEYCODE_BACK

甚至還有覆寫 View 的 dispatchKeyEvent() 等函數處理 KEYCODE_BACK

5.2 適配

適配的目的在於確保如下:

12 及以前的設備上 Back Gesture、Back KeyButton 以及其他 Key 抵達的時候,onKeyUp() 等回調能正常收到
13 上開啟新返回導航支持的話:Back Gesture 和 Back KeyButton 能在對應的 Callback 里回調,並和之前的 Back 動作保持一致。同時,其他 Key 仍能在 onKeyUp() 等原有函數裡監聽到

以上述的案例 1 的代碼為例,如下是如何改造以保證能在 12 和 13 上運行一樣的 Key 相關動作:

classActivity{privatevaronBackInvokedCallback:OnBackInvokedCallback?=nulloverridefunonCreate(savedInstanceState:Bundle?){...if(BuildCompat.isAtLeastT()){onBackInvokedCallback=OnBackInvokedCallback{onBackEvent()}.also{onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT,it)}}}overridefunonDestroy(){super.onDestroy()if(BuildCompat.isAtLeastT()){onBackInvokedCallback?.let{onBackInvokedDispatcher.unregisterOnBackInvokedCallback(it)}}}privatefunonBackEvent(){//if(...)returnfalseif(...)return//when(keyCode){//KeyEvent.KEYCODE_BACK->{methodA()}//KeyEvent.KEYCODE_MENU->{...}//else->{}//}methodA()//returnif(...){//true//}elsesuper.onKeyDown(keyCode,event)}//為兼容舊版仍需完全保留overridefunonKeyDown(keyCode:Int,event:KeyEvent?):Boolean{...}}

如上適配的關鍵點在於:除了在 Manifest 中將 enableOnBackInvokedCallback 屬性打開和註冊 OnBackInvokedCallback() 以外,重點在於如何實現 onBackInvoked() 來達到舊版的同等返回邏輯:

刪除掉 Back KeyButton 以外的邏輯,因為 Callback 只針對 Back 事件,沒有可能收到其他 KEY 事件
刪除掉 KEYCODE_BACK 的檢查,因為 Callback 只針對 Back 事件、沒有必要檢查
按照原有的 dispatchtKeyEvent()、onKeyDown()、onKeyUp() 的邏輯決定 return true、false 以及 super 的改寫辦法
兼容舊版本保留所有的 KeyEvent 的處理邏輯

此外,需要留意如下一些細節:

新的 Callback 如何區分 dispatchKeyEvent()、onKeyDown()、onKeyUp() 的時機?

無法區分,開啟新返回導航之後只有一個 OnBackInvokedCallback 回調時機,其在Back Gesture Trigger或Back KeyButton Up時觸發。

原本時序:dispatchKeyEvent(DOWN) -> onKeyDown() -> dispatchKeyEvent(UP) -> onKeyUp()

新的 Callback 如何針對 KEYCODE_BACK 的 DOWN 和 UP 作區分?

無法區分,開啟新返回導航之後只有最終的 Callback,沒有 DOWN 和 UP 之分。

新的 Callback 針對 dispatchKeyEvent() 等處理的 return true、false、super 如何區分?

false:本意是不處理,對應於現在的 Callback 可以是什麼也不做或直接 return
true:本意是處理,對應於現在的 Callback 可以是處理外加 return
super:本意是交由父類處理,對應於現在的 Callback 可以是 return 或者直接刪除,這取決於原來的 super 調用位置,也可以考慮在某條件滿足的時候提前註銷 Callback這種思路

Back 以外,比如 Menu KeyEvent 的監聽是否受影響?

不受影響。之前的 Menu Key 等監聽在 13 上仍可以監聽到、正常運行,可以保留。

如何兼容 13 以前的版本呢?

新老處理共存,判斷運行版本:13 上開啟的話執行新邏輯,13 以前繼續沿用舊邏輯。

5.3 集成到 Base 中統一處理

Activity、Fragment 以及 Dialog 眾多的情況下,可在 Base 類里加入統一的註冊和銷毀 Callback 的復用代碼。

為了不干預不需要處理的子類,默認不進行註冊。需要的子類覆寫isNeedInterceptBackEvent()返回 true 並實現自己的 Callback 邏輯即可。

如下的 BaseActivity 事例代碼:

openclassBaseActivity:AppCompatActivity(){privatevaronBackInvokedCallback:OnBackInvokedCallback?=null/***Innerclassforhandlebackcallbacktotally.*/internalclassOnBackInvokedCallbackInnerconstructor(baseActivity:BaseActivity):OnBackInvokedCallback{privatevalactivity:WeakReference<BaseActivity>overridefunonBackInvoked(){activity.get()?.apply{onBackEvent()}}init{activity=WeakReference(baseActivity)}}/***Overridethismethodandreturntrueifchildwannahandlebackevent.*/openfunisNeedInterceptBackEvent():Boolean=false/***DefaultbackoperationisinvokingonBackPressed().*Childactivitycouldoverrideandimplementitsownoperation.*/openfunonBackEvent(){onBackPressed()}overridefunonCreate(savedInstanceState:Bundle?){...if(isNeedInterceptBackEvent()&&BuildCompat.isAtLeastT()){onBackInvokedCallback=OnBackInvokedCallbackInner(this).also{onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT,it)}}}overridefunonDestroy(){...if(BuildCompat.isAtLeastT()){onBackInvokedCallback?.let{onBackInvokedDispatcher.unregisterOnBackInvokedCallback(it)}}}}

需要的子類進行覆寫。

classBackKeyHandleActivity:BaseActivity(){...overridefunisNeedInterceptBackEvent():Boolean=trueoverridefunonBackEvent(){...}//兼容13之前的邏輯overridefunonBackPressed(){...}}6. 新返回導航支持與否的深入比較和原理分析

針對採用新 SDK 返回 API 方案分別在 13 上開啟和關閉新返回導航的支持,觀察 KeyEvent 相關的 Log 輸出,並嘗試分析一些原理方面的差異。

6.1 開啟支持Back Gesture

開啟新的返回手勢支持的話,只能收到 OnBackInvokedCallback 回調,確實無法像以前一樣靈活、精細地處理 KEYCODE_BACK 了。

如下的系統日誌可以瞥見 Callback 處理的一些細節。

05-2610:26:27.929787787DNoBackGesture:Startgesture:MotionEvent{action=ACTION_DOWN...}05-2610:26:27.929787787DNoBackGesture:Prediction[1653531987929,47,633,-1,0.000000,1]05-2610:26:27.930787787DNoBackGesture:resetmTriggerBack=false05-2610:26:27.931787852DShellBackPreview:initAnimationmMotionStarted=false05-2610:26:27.932787787DNoBackGesture:Gesture[1653531987932,alw=TRUE,TRUE,TRUE,FALSE,disp=Point(1080,2340),wl=82,il=0,wr=82,ir=0,excl=SkRegion()]05-2610:26:27.9335992725DCoreBackPreview:FocusedwindowfoundusinggetFocusedWindowToken05-2610:26:27.9335992725DCoreBackPreview:startBackNavigationcurrentTask=Task{1d3c440#502type=...},callbackInfo=OnBackInvokedCallbackInfo{...}05-2610:26:27.934787852DShellBackPreview:ReceivedbackNavigationInfo:BackNavigationInfo{...}05-2610:26:27.963787787DOnBackInvokedDispatcher:ViewRootImpl.registerBackCallbackOnWindow.Dispatcher:android.window.WindowOnBackInvokedDispatcher@be64a11Package:com.android.systemuiIWindow:android.view.ViewRootImpl$W@5c4e776Session:android.view.IWindowSession$Stub$Proxy@3998bd705-2610:26:27.968787787VOnBackInvokedDispatcher:ProxysetActualandroid.window.WindowOnBackInvokedDispatcher@be64a11.Currentnull05-2610:26:27.968787787VOnBackInvokedDispatcher:Proxytransferring0callbackstoandroid.window.WindowOnBackInvokedDispatcher@be64a1105-2610:26:28.27139783978DBackGesture:onBackInvoked()

通過 adb shell dumpsys input 命令確實也沒有看到 InputFlinger 發送 KEYCODE_BACK 的記錄。

MotionEvent(deviceId=8,eventTime=2965229468000,source=TOUCHSCREEN|STYLUS,displayId=0,action=DOWN...)MotionEvent(deviceId=8,eventTime=2965457324000,source=TOUCHSCREEN|STYLUS,displayId=0,action=MOVE...)...MotionEvent(deviceId=8,eventTime=2965524225000,source=TOUCHSCREEN|STYLUS,displayId=0,action=UP...)Back KeyButton

Back KeyButton 場景也是一樣,開啟新返回導航支持的話,只能收到 OnBackInvokedCallback 回調。

05-2610:59:05.85444974497DOnBackInvokedDispatcher:ViewRootImpl.registerBackCallbackOnWindow.Dispatcher:android.window.WindowOnBackInvokedDispatcher@ad0c7c7Package:com.android.systemuiIWindow:android.view.ViewRootImpl$W@2ade1f4Session:android.view.IWindowSession$Stub$Proxy@f606e1705-2610:59:05.90444974497VOnBackInvokedDispatcher:ProxysetActualandroid.window.WindowOnBackInvokedDispatcher@ad0c7c7.Currentnull05-2610:59:05.90444974497VOnBackInvokedDispatcher:Proxytransferring0callbackstoandroid.window.WindowOnBackInvokedDispatcher@ad0c7c705-2610:59:05.97777007700DBackGesture:onBackInvoked()05-2610:59:06.49577007700VOnBackInvokedDispatcher:Proxyunregisterandroid.app.Activity$$ExternalSyntheticLambda0@a72f76.Actual=android.window.WindowOnBackInvokedDispatcher@1da92aa05-2610:59:06.49577007700VOnBackInvokedDispatcher:Proxyunregistercom.example.tiramisu_demo.MainActivity$$ExternalSyntheticLambda1@d37f96a.Actual=android.window.WindowOnBackInvokedDispatcher@1da92aa05-2610:59:27.69644974497DOnBackInvokedDispatcher:ViewRootImpl.registerBackCallbackOnWindow.Dispatcher:android.window.WindowOnBackInvokedDispatcher@cdfd9cPackage:com.android.systemuiIWindow:android.view.ViewRootImpl$W@da2b3a5Session:android.view.IWindowSession$Stub$Proxy@f606e1705-2610:59:27.70744974497VOnBackInvokedDispatcher:ProxysetActualandroid.window.WindowOnBackInvokedDispatcher@cdfd9c.Currentnull05-2610:59:27.70744974497VOnBackInvokedDispatcher:Proxytransferring0callbackstoandroid.window.WindowOnBackInvokedDispatcher@cdfd9c

但 dump input 卻出現了 Back 的 KeyEvent 記錄,這是為什麼呢?

此處留個懸念,後面會揭開謎底。

MotionEvent(deviceId=8,eventTime=2276120343000,source=TOUCHSCREEN|STYLUS,displayId=0,action=DOWN...)KeyEvent(deviceId=-1,eventTime=2276124000000,source=KEYBOARD,displayId=0,action=DOWN,flags=0x00000048,keyCode=BACK(4)...)MotionEvent(deviceId=8,eventTime=2276205324000,source=TOUCHSCREEN|STYLUS,displayId=0,action=UP...)KeyEvent(deviceId=-1,eventTime=2276266000000,source=KEYBOARD,displayId=0,action=UP,flags=0x00000048,keyCode=BACK(4)...)6.2 關閉支持Back Gesture

當關閉支持後 Back Gesture 場景下能和舊版本一樣收到 KEYCODE_BACK 了。

05-2611:09:28.23567846784DBackGesture:dispatchKeyEvent()event:KeyEvent{action=ACTION_DOWN,keyCode=KEYCODE_BACK...}05-2611:09:28.23667846784DBackGesture:dispatchKeyEvent()event:KeyEvent{action=ACTION_UP,keyCode=KEYCODE_BACK...}05-2611:09:28.24067846784DBackGesture:onBackPressed()

dump input 也可以證實該 KeyEvent 的真實存在,而且可以看到 Back Gesture 的 UP 之後連續注入了 KEYCODE_BACK 的 DOWN 和 UP 的細節。

MotionEvent(deviceId=8,eventTime=585598303000,source=0x00005002,displayId=0,action=DOWN...)MotionEvent(deviceId=8,eventTime=585812734000,source=0x00005002,displayId=0,action=MOVE...)...MotionEvent(deviceId=8,eventTime=585858936000,source=0x00005002,displayId=0,action=UP...)KeyEvent(deviceId=-1,eventTime=585859000000,source=0x00000101,displayId=0,action=DOWN,flags=0x00000048,keyCode=4...)KeyEvent(deviceId=-1,eventTime=585860000000,source=0x00000101,displayId=0,action=UP,flags=0x00000048,keyCode=4...)Back KeyButton

自不必說,Back KeyButton 的按下當然也可以收到 KEYCODE_BACK。

05-2610:48:21.58058175817DBackGesture:dispatchKeyEvent()event:KeyEvent{action=ACTION_DOWN,keyCode=KEYCODE_BACK...}05-2610:48:21.63558175817DBackGesture:dispatchKeyEvent()event:KeyEvent{action=ACTION_UP,keyCode=KEYCODE_BACK...}05-2610:48:21.63558175817DBackGesture:onBackPressed()

但與 Gesture 不同,dump input 的結果可以看到:在 Back KeyButton 上按下時注入了 KEYCODE_BACK 的 DOWN,抬起注入了 UP。

MotionEvent(deviceId=8,eventTime=352268883000,source=0x00005002,displayId=0,action=DOWN...)KeyEvent(deviceId=-1,eventTime=352289000000,source=0x00000101,displayId=0,action=DOWN...)MotionEvent(deviceId=8,eventTime=352378721000,source=0x00005002,displayId=0,action=UP...)KeyEvent(deviceId=-1,eventTime=352386000000,source=0x00000101,displayId=0,action=UP...)6.3 Back 相關時序的變化總結
對比點關閉支持-Back Gesture關閉支持-Back KeyButton開啟支持-Back Gesture開啟支持-Back KeyButtonViewRootImpl#processKeyEvent()YESYESNOYESdispatchKeyEvent(DOWN)YEStrigger 時連續發送 DOWN 和 UPYES按下時發送 DOWNNONOonKeyDown()YESYESNONOdispatchKeyEvent(UP)YESYES抬起時發送 UPNONOonKeyUp()YESYESNONOonBackPressed()YESYESNONOOnBackInvokedCallbackNONOYESYES
6.4 開啟支持的原理分析
13 開啟支持12Back GestureCallback + KEYCODE_BACK 無法監聽KEYCODE_BACK 可以監聽Back KeyButtonCallback + KEYCODE_BACK 無法監聽但 KEYCODE_BACK 實際存在KEYCODE_BACK 可以監聽

13 上開啟支持之後,如果是點擊 Back KeyButton,從 dump 來看仍然發出了 KEYCODE_BACK,猜測與你大體是這樣:

Back Gesture 觸發的時候,如果發現支持了 新返回導航,那就不再注入 KEYCODE_BACK,而是通過 Binder 告知 App 進程直接處理 Callback 的回調
Back KeyButton 仍然像以前一樣注入 KEYCODE_BACK,但 ViewRootImpl 接收到該事件的時候,發現支持了 新返回導航,則沒有向 View 樹分發,而是取出 Callback 直接回調

這裡不禁產生一個疑問:

Back Gesture 和 Back KeyButton 緣何沒有採用同一個處理方式?

經過思考,覺得不免又如下幾點可能:

Back Gesture 和 Back KeyButton 功能定位有區別:前者是返回手勢,需要展示返回圖標和背面視圖的動畫,它的處理在 EdgeBackGesture 里;後者是虛擬按鍵,在 NavigationBar 的 KeyButtonView 中處理
13 之前沒有引入可預測型動畫的時候兩者功能雷同,所以 Back Gesture 採用了和 Back KeyButton 一樣的邏輯
13 引入了和 Back KeyButton 完全不同的返回預測動畫,需要實現一套自己的回調路徑,不需要再依賴原來的 KeyEvent 路徑
另外從是否屬於按鍵的角度上來講
Back Gesture 不是虛擬按鍵、也不是實體按鍵,沒有必要發送 KeyEvent
Back KeyButton 是虛擬按鍵,需要遵從 Key 的 Map 規範,是需要發送對應 KeyEvent 出來的。而且即便後面會被 App 攔截,但對於前期的系統 PhoneWindowManager、InputFilter 可能也需要處理

需要說明的是,當關閉新返回導航支持後,為了兼容舊的 API,Back Gesture 仍像以前一樣發送 KEYCODE_BACK。當然這肯定是暫時的,後續系統肯定會強制使用該特性,到時候這個 Back Gesture 就再也不用發送 KEYCODE_BACK 了。

7. 注意和殘留事項
本次變更跟TargetSDKVersion無關,運行在 13 上的 App 都需要思考是否收到影響、如何適配
直到 Android 13 最終版可預見型返回手勢的動畫才能生效:Settings > System > Developer options > Predictive back animations
新 SDK 返回 APIOnBackInvokedDispatcher中註冊的OnBackInvokedCallback回調不是按照文檔描述的逆序,而是只回調最後一個高優先級的 Callback
Manifest 文件里enableOnBackInvokedCallback屬性關閉的話,不要殘留註冊 OnBackInvokedCallback 的邏輯,不然新返回導航可能仍然有效
Dialog場景使用新版 SDK 返回 API 沒有效果,原因未知
View監聽 KEYCODE_BACK 的邏輯是否受影響,暫未實驗
對於新 SDK 返回 API 的註冊和銷毀的時機可以選擇:onCreate()+onDestroy(),onCreate() + onStop()、onResume() + onPause() 的組合亦可,但要注意是否會發生畫面展示前的 Back Gesture 或 Back KeyButton 無法被監聽以及畫面進入後台了但 Callback 未被註銷等問題。當然註冊和註銷的時機可依據需要的條件靈活選擇,沒有絕對的要求
使用 AndroidX API 方案要注意升級 AppCompat 到1.6.0-alpha04,不然不生效
另外,採用 AndroidX API 方案但關閉了支持的話,Back Gesture 沒有像 Back KeyButton 一樣,只能收到OnBackPressedCallback,沒有 KeyEvent 回調,原因未知
對於某些場景下不希望 Callback 而希望系統處理的話,對於 SDK API 而言可以使用unregister方法註銷該 Callback;對於 AndroidX API 而言可以將 Callback 狀態置為disabled
總結

製作了一張 Android 13 新返回導航適配流程圖供大家快速查閱。

做個簡單總結:

如果決定支持新返回導航即聲明enableOnBackInvokedCallback為 true,之後需依據 App 集成了 SDK API 還是 AndroidX API 決定適配的方案。

SDK 方案的話需要引入新的OnBackInvokedDispatcher相關API,並留意 Activity、Dialog、Window、View 上現有的 Back 邏輯是否會收到影響,以及如何改造。當然需要判斷運行版本,並為了兼容13之前的設備保留現有的 Back 邏輯
AndroidX 方案的話使用專屬的OnBackPressedDispatcherAPI,AppCompat庫升級之後會自行完成內部的 SDK API 遷移

另外還需要留意上述章節提及的注意事項和殘留事項。

當然如果沒有餘力適配,決定捨棄可預測型返回手勢、OnBackInvokedDispatcher 新 API 以及KEYCODE_BACK等一系列變更,可以選擇什麼也不做。

但早在 13 之前,官方已推薦使用 AndroidX 的 OnBackPressedDispatcher 來取代onBackPressed,13 花這麼大精力完全廢棄 onBackPressed 並向 AOSP 新增了 OnBackInvokedDispatcher 等系列 API。

從這個趨勢來看,估計到 Android 14 這個新返回導航就會成為強制要求,開發者們當儘早適配才是!

參考

官方文檔:

更新你的 app 去支持可預見型返回手勢:https://developer.android.google.cn/about/versions/13/features/predictive-back-gesture
如何提供自定義返回導航:https://developer.android.google.cn/guide/navigation/navigation-custom-back?hl=zh-cn

SDK API:

OnBackInvokedCallback: https://developer.android.google.cn/reference/android/window/OnBackInvokedCallback
OnBackInvokedDispatcher:https://developer.android.google.cn/reference/android/window/OnBackInvokedDispatcher
Activity#getOnBackInvokedDispatcher:https://developer.android.google.cn/reference/android/app/Activity#getOnBackInvokedDispatcher()
Dialog#getOnBackInvokedDispatcher():https://developer.android.google.cn/reference/android/app/Dialog#getOnBackInvokedDispatcher()
Window#getOnBackInvokedDispatcher():https://developer.android.google.cn/reference/android/view/Window#getOnBackInvokedDispatcher()

AndroidX API:

OnBackPressedDispatcher:https://developer.android.google.cn/reference/androidx/activity/OnBackPressedDispatcher
OnBackPressedCallback:https://developer.android.google.cn/reference/androidx/activity/OnBackPressedCallback


為了防止失聯,歡迎關注我防備的小號


微信改了推送機制,真愛請星標本公號👇

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()