5. SDK API 适配方案的深入探讨
5.1 案例
和 KEYCODE_BACK 相关的有很多 API 可以处理、场景也很繁杂,简单举例如下:
- 覆写 Activity#onKeyDown() 处理 KEYCODE_BACK 的 DOWN:
class Activity { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { if ( ... ) return false when (keyCode) { KeyEvent.KEYCODE_BACK -> { methodA() } KeyEvent.KEYCODE_MENU -> { ... } else -> {} } return if ( ... ) { true } else super.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 相关动作:
class Activity { private var onBackInvokedCallback: OnBackInvokedCallback? = null override fun onCreate(savedInstanceState: Bundle?) { ... if (BuildCompat.isAtLeastT()) { onBackInvokedCallback = OnBackInvokedCallback { onBackEvent() }.also { onBackInvokedDispatcher.registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_DEFAULT, it ) } } } override fun onDestroy() { super.onDestroy() if (BuildCompat.isAtLeastT()) { onBackInvokedCallback?.let { onBackInvokedDispatcher.unregisterOnBackInvokedCallback(it) } } } private fun onBackEvent() { // if ( ... ) return false if ( ... ) return // when (keyCode) { // KeyEvent.KEYCODE_BACK -> { methodA() } // KeyEvent.KEYCODE_MENU -> { ... } // else -> {} // } methodA() // return if ( ... ) { // true // } else super.onKeyDown(keyCode, event) } // 为兼容旧版仍需完全保留 override fun onKeyDown(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 事例代码:
open class BaseActivity: AppCompatActivity() { private var onBackInvokedCallback: OnBackInvokedCallback? = null /** * Inner class for handle back callback totally. */ internal class OnBackInvokedCallbackInner constructor(baseActivity: BaseActivity) : OnBackInvokedCallback { private val activity: WeakReference<BaseActivity> override fun onBackInvoked() { activity.get()?.apply { onBackEvent() } } init { activity = WeakReference(baseActivity) } } /** * Override this method and return true if child wanna handle back event. */ open fun isNeedInterceptBackEvent(): Boolean = false /** * Default back operation is invoking onBackPressed(). * Child activity could override and implement its own operation. */ open fun onBackEvent() { onBackPressed() } override fun onCreate(savedInstanceState: Bundle?) { ... if (isNeedInterceptBackEvent() && BuildCompat.isAtLeastT()) { onBackInvokedCallback = OnBackInvokedCallbackInner(this).also { onBackInvokedDispatcher.registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_DEFAULT, it ) } } } override fun onDestroy() { ... if (BuildCompat.isAtLeastT()) { onBackInvokedCallback?.let { onBackInvokedDispatcher.unregisterOnBackInvokedCallback(it) } } } }
需要的子类进行覆写。
class BackKeyHandleActivity : BaseActivity() { ... override fun isNeedInterceptBackEvent(): Boolean = true override fun onBackEvent() { ... } // 兼容 13 之前的逻辑 override fun onBackPressed() { ... } }
6. 新返回导航支持与否的深入比较和原理分析
针对采用新 SDK 返回 API 方案分别在 13 上开启和关闭新返回导航的支持,观察 KeyEvent 相关的 Log 输出,并尝试分析一些原理方面的差异。
6.1 开启支持
Back Gesture
开启新的返回手势支持的话,只能收到 OnBackInvokedCallback 回调,确实无法像以前一样灵活、精细地处理 KEYCODE_BACK 了。
如下的系统日志可以瞥见 Callback 处理的一些细节。
05-26 10:26:27.929 787 787 D NoBackGesture: Start gesture: MotionEvent { action=ACTION_DOWN ... } 05-26 10:26:27.929 787 787 D NoBackGesture: Prediction [1653531987929,47,633,-1,0.000000,1] 05-26 10:26:27.930 787 787 D NoBackGesture: reset mTriggerBack=false 05-26 10:26:27.931 787 852 D ShellBackPreview: initAnimation mMotionStarted=false 05-26 10:26:27.932 787 787 D NoBackGesture: Gesture [1653531987932,alw=TRUE,TRUE,TRUE,FALSE,disp=Point(1080, 2340),wl=82,il=0,wr=82,ir=0,excl=SkRegion()] 05-26 10:26:27.933 599 2725 D CoreBackPreview: Focused window found using getFocusedWindowToken 05-26 10:26:27.933 599 2725 D CoreBackPreview: startBackNavigation currentTask=Task{1d3c440 #502 type= ...}, callbackInfo=OnBackInvokedCallbackInfo{ ... } 05-26 10:26:27.934 787 852 D ShellBackPreview: Received backNavigationInfo:BackNavigationInfo{...} 05-26 10:26:27.963 787 787 D OnBackInvokedDispatcher: ViewRootImpl.registerBackCallbackOnWindow. Dispatcher:android.window.WindowOnBackInvokedDispatcher@be64a11 Package:com.android.systemui IWindow:android.view.ViewRootImpl$W@5c4e776 Session:android.view.IWindowSession$Stub$Proxy@3998bd7 05-26 10:26:27.968 787 787 V OnBackInvokedDispatcher: Proxy setActual android.window.WindowOnBackInvokedDispatcher@be64a11. Current null 05-26 10:26:27.968 787 787 V OnBackInvokedDispatcher: Proxy transferring 0 callbacks to android.window.WindowOnBackInvokedDispatcher@be64a11 05-26 10:26:28.271 3978 3978 D BackGesture: 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-26 10:59:05.854 4497 4497 D OnBackInvokedDispatcher: ViewRootImpl.registerBackCallbackOnWindow. Dispatcher:android.window.WindowOnBackInvokedDispatcher@ad0c7c7 Package:com.android.systemui IWindow:android.view.ViewRootImpl$W@2ade1f4 Session:android.view.IWindowSession$Stub$Proxy@f606e17 05-26 10:59:05.904 4497 4497 V OnBackInvokedDispatcher: Proxy setActual android.window.WindowOnBackInvokedDispatcher@ad0c7c7. Current null 05-26 10:59:05.904 4497 4497 V OnBackInvokedDispatcher: Proxy transferring 0 callbacks to android.window.WindowOnBackInvokedDispatcher@ad0c7c7 05-26 10:59:05.977 7700 7700 D BackGesture: onBackInvoked() 05-26 10:59:06.495 7700 7700 V OnBackInvokedDispatcher: Proxy unregister android.app.Activity$$ExternalSyntheticLambda0@a72f76. Actual=android.window.WindowOnBackInvokedDispatcher@1da92aa 05-26 10:59:06.495 7700 7700 V OnBackInvokedDispatcher: Proxy unregister com.example.tiramisu_demo.MainActivity$$ExternalSyntheticLambda1@d37f96a. Actual=android.window.WindowOnBackInvokedDispatcher@1da92aa 05-26 10:59:27.696 4497 4497 D OnBackInvokedDispatcher: ViewRootImpl.registerBackCallbackOnWindow. Dispatcher:android.window.WindowOnBackInvokedDispatcher@cdfd9c Package:com.android.systemui IWindow:android.view.ViewRootImpl$W@da2b3a5 Session:android.view.IWindowSession$Stub$Proxy@f606e17 05-26 10:59:27.707 4497 4497 V OnBackInvokedDispatcher: Proxy setActual android.window.WindowOnBackInvokedDispatcher@cdfd9c. Current null 05-26 10:59:27.707 4497 4497 V OnBackInvokedDispatcher: Proxy transferring 0 callbacks to android.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-26 11:09:28.235 6784 6784 D BackGesture: dispatchKeyEvent() event:KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK ... } 05-26 11:09:28.236 6784 6784 D BackGesture: dispatchKeyEvent() event:KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK ... } 05-26 11:09:28.240 6784 6784 D BackGesture: 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-26 10:48:21.580 5817 5817 D BackGesture: dispatchKeyEvent() event:KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK...} 05-26 10:48:21.635 5817 5817 D BackGesture: dispatchKeyEvent() event:KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK ...} 05-26 10:48:21.635 5817 5817 D BackGesture: 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 相关时序的变化总结
6.4 开启支持的原理分析
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 返回 API OnBackInvokedDispatcher 中注册的 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 方案的话使用专属的OnBackPressedDispatcher API,AppCompat 库升级之后会自行完成内部的 SDK API 迁移
另外还需要留意上述章节提及的注意事项和残留事项。
当然如果没有余力适配,决定舍弃可预测型返回手势、OnBackInvokedDispatcher 新 API 以及 KEYCODE_BACK 等一系列变更,可以选择什么也不做。
但早在 13 之前,官方已推荐使用 AndroidX 的 OnBackPressedDispatcher 来取代 onBackPressed,13 花这么大精力完全废弃 onBackPressed 并向 AOSP 新增了 OnBackInvokedDispatcher 等系列 API。
从这个趋势来看,估计到 Android 14 这个新返回导航就会成为强制要求,开发者们当尽早适配才是!
参考
官方文档:
更新你的 app 去支持可预见型返回手势
如何提供自定义返回导航
SDK API:
OnBackInvokedCallback
OnBackInvokedDispatcher
Activity#getOnBackInvokedDispatcher
Dialog#getOnBackInvokedDispatcher()
Window#getOnBackInvokedDispatcher()
AndroidX API:
OnBackPressedDispatcher
OnBackPressedCallback