Android 13 返回导航大变更:返回键彻底废弃 + 可预见型返回手势(2)

简介: Android 13 返回导航大变更:返回键彻底废弃 + 可预见型返回手势(2)

5. SDK API 适配方案的深入探讨

5.1 案例

和 KEYCODE_BACK 相关的有很多 API 可以处理、场景也很繁杂,简单举例如下:

  1. 覆写 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 相关时序的变化总结

1672191159956.png

6.4 开启支持的原理分析

1672191179936.png

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 新返回导航适配流程图供大家快速查阅。

1832b220aa754cd18c504acc7686a560.png

做个简单总结:


如果决定支持新返回导航即声明 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


相关文章
|
API 开发工具 Android开发
解决 Android App 上架 Google play后 ,签名变更,第三方sdk无法登录
解决 Android App 上架 Google play后 ,签名变更,第三方sdk无法登录
289 0
|
6月前
|
XML Java Android开发
Android Studio App开发之捕获屏幕的变更事件实战(包括竖屏与横屏切换,回到桌面与切换到任务列表)
Android Studio App开发之捕获屏幕的变更事件实战(包括竖屏与横屏切换,回到桌面与切换到任务列表)
196 0
|
6月前
|
XML 监控 Android开发
Android Studio App开发入门之文本输入EditText的讲解及使用(附源码 包括编辑框、焦点变更监听器、文本变化监听器 )
Android Studio App开发入门之文本输入EditText的讲解及使用(附源码 包括编辑框、焦点变更监听器、文本变化监听器 )
282 0
|
4月前
|
Android开发 Kotlin
kotlin开发安卓app,如何让布局自适应系统传统导航和全面屏导航
使用`navigationBarsPadding()`修饰符实现界面自适应,自动处理底部导航栏的内边距,再加上`.padding(bottom = 10.dp)`设定内容与屏幕底部的距离,以完成全面的布局适配。示例代码采用Kotlin。
127 15
|
6月前
|
Java Android开发
Android 导航方式切换
Android 导航方式切换
128 1
|
6月前
|
Android开发
【Android 从入门到出门】第四章:现代Android开发中的导航
【Android 从入门到出门】第四章:现代Android开发中的导航
43 2
【Android 从入门到出门】第四章:现代Android开发中的导航
|
6月前
|
Java 定位技术 Android开发
【Android App】利用腾讯地图获取地点信息和规划导航线路讲解及实战(附源码和演示视频 超详细必看)
【Android App】利用腾讯地图获取地点信息和规划导航线路讲解及实战(附源码和演示视频 超详细必看)
376 1
|
6月前
|
XML Java 定位技术
【Android App】定位导航GPS中开启手机定位功能讲解及实战(附源码和演示 超详细)
【Android App】定位导航GPS中开启手机定位功能讲解及实战(附源码和演示 超详细)
297 0
|
6月前
|
XML Java Android开发
Android Studio App开发之监听系统广播Broadcast的讲解及实战(包括接收分钟到达广播、网络变更广播、定时管理器等 附源码)
Android Studio App开发之监听系统广播Broadcast的讲解及实战(包括接收分钟到达广播、网络变更广播、定时管理器等 附源码)
438 0
|
存储 Android开发
android ViewPager + Fragment + Tablayout 实现嵌套页面导航(二)
android ViewPager + Fragment + Tablayout 实现嵌套页面导航
android ViewPager + Fragment + Tablayout 实现嵌套页面导航(二)