Android 10 首次引入了全局返回手势,但直到返回触发才能看到目标上层画面。13 针对该特性进行了优化,即返回触发之前可以预览上层画面。同时彻底废弃了返回键相关的 API,这将对现有的 App 逻辑产生巨大的影响!
前言
Android 13 针对包括手机、大屏、折叠屏等 Android 设备推出了可预见型返回手势(Predictive Back Gesture
)特性。该特性将便于用户在返回完成之前可以先预览到目标画面或结果,这样的话可以允许他们决定是否要继续返回或者放弃并停留在当前画面。
另外引入关于 KEYCODE_BACK KeyEvent 相关的一系列变更。
为节省篇幅和统一认识,后续的相关描述将按照如下规则简称:
本次引入的可预见型返回手势 + KEYCODE_BACK 系列变更:统称为新返回导航
KEYCODE_BACK KeyEvent:简称为 KEYCODE_BACK
传统导航模式和 Swipe-Up 导航模式下的返回按钮:简称为Back KeyButton
全局返回手势:简称为Back Gesture
Back KeyButton | Back 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 进行分情况适配。
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 the OnBackPressedCallback AppCompat 1.6.0-alpha03 (AndroidX) or higher API.笔者按照官方说明将 AppCompat
包升级到了 1.6.0-alpha03
。
dependencies { implementation 'androidx.appcompat:appcompat:1.6.0-alpha03' }
使用其提供的 OnBackPressedCallback
API 监听 Activity 的 Back 操作如下:
class BackKeyTestActivityAppCompat : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { Log.d("BackGesture", "Activity#handleOnBackPressed()") } }) } override fun dispatchKeyEvent(event: KeyEvent): Boolean { Log.d("BackGesture", "Activity#dispatchKeyEvent() event:$event") return super.dispatchKeyEvent(event) } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { Log.d("BackGesture", "Activity#onKeyDown() event:$event") return super.onKeyDown(keyCode, event) } override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { Log.d("BackGesture", "Activity#onKeyUp() event:$event") return super.onKeyUp(keyCode, event) } override fun onBackPressed() { 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-31 10:35:28.732 11267 11267 D BackGesture: Activity#dispatchKeyEvent() event:KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK, ... } 05-31 10:35:28.733 11267 11267 D BackGesture: Activity#onKeyDown() event:KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK, ... } 05-31 10:35:28.733 11267 11267 D BackGesture: Activity#dispatchKeyEvent() event:KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK, ... } 05-31 10:35:28.733 11267 11267 D BackGesture: Activity#onKeyUp() event:KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK, ... } 05-31 10:35:28.733 11267 11267 D BackGesture: onBackPressed() 05-31 10:35:28.734 11267 11267 D BackGesture: Activity#handleOnBackPressed()
12-Back KeyButton 的执行日志:
05-31 10:37:21.724 11267 11267 D BackGesture: Activity#dispatchKeyEvent() event:KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK... } 05-31 10:37:21.724 11267 11267 D BackGesture: Activity#onKeyDown() event:KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK... } 05-31 10:37:21.846 11267 11267 D BackGesture: Activity#dispatchKeyEvent() event:KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK... } 05-31 10:37:21.846 11267 11267 D BackGesture: Activity#onKeyUp() event:KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK... } 05-31 10:37:21.846 11267 11267 D BackGesture: onBackPressed() 05-31 10:37:21.846 11267 11267 D BackGesture: Activity#handleOnBackPressed()
调试了一下,发现 AppCompat 框架里使用 13 的新 SDK API 前的版本判断有问题:
public class ComponentActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { ... if (Build.VERSION.SDK_INT >= 33) { mOnBackPressedDispatcher.setOnBackInvokedDispatcher(getOnBackInvokedDispatcher()); } ... } } public final class OnBackPressedDispatcher { Cancellable addCancellableCallback(@NonNull OnBackPressedCallback onBackPressedCallback) { ... if (Build.VERSION.SDK_INT >= 33) { updateBackInvokedCallbackState(); onBackPressedCallback.setIsEnabledConsumer(mEnabledConsumer); } return cancellable; } }
Beta
版的 SDK_INT
常量仍然是 12L
的 32,到正式发布才会改为 33,所以版本判断应当使用 BuildCompat
的如下 API:
// BuildCompat.java public static boolean isAtLeastT() { return VERSION.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-31 10:55:10.773 5041 5041 D BackGesture: Activity#handleOnBackPressed()
但仍有一点未达预期:
按理说 13 上关闭支持的话,无论是 Back Gesture 还是 Back KeyButton,运行结果应该和 12 保持一致,即收到 Back 相关 KeyEvent 回调以及 OnBackPressedCallback
可实测发现:只有 Back KeyButton 点击是上述结果,Back Gesture 的话只收到了 Callback、没有 KeyEvent 回调,这里有点奇怪
4.4 迁移非推荐 SDK 返回 API 到 AndroidX API
适配步骤:
迁移已有的系统返回处理逻辑到 AndroidX 的 OnBackPressedDispatcher
API,他需要指定 OnBackPressedCallback 实现,详细的可参考如何提供自定义返回导航
对于 Activity:
class MyActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val callback = onBackPressedDispatcher.addCallback(this) { // Handle the back button event } } ... }
对于 Fragment:
public class FormEntryFragment extends Fragment { @Override public void onAttach(@NonNull Context context) { super.onAttach(context); OnBackPressedCallback callback = new OnBackPressedCallback( true // default to enabled ) { @Override public void handleOnBackPressed() { showAreYouSureDialog(); } }; requireActivity().getOnBackPressedDispatcher().addCallback( this, // LifecycleOwner callback); } }
禁用原有的系统返回手势回调,比如 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 app MUST unregister the OnBackInvokedCallback. 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.
@Override void onCreate() { if (BuildCompat.isAtLeastT()) { getOnBackInvokedDispatcher().registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_DEFAULT, () -> { // ... } ); } }
比如 WebView 需要拦截返回手势以回退网页,当已经返回到主画面的时候应当注销该 Callback 让系统来处理 finish。
同样的,加入新返回导航的支持。
备注:onBackPressed() 逻辑保留也没有关系,并不会发生冲突,而且为了兼容 13 之前的系统功能本就应该保留。
registerOnBackInvokedCallback() 说明
registerOnBackInvokedCallback() 调用的时候需要提供如下两个参数:
priority:按照注册的逆序进行,但如果是高优先级的先回调。可选范围:int 型,亦可选如下预设常量:
PRIORITY_DEFAULT:值为 0,普通回调
PRIORITY_OVERLAY:值为 1000000,优先回调
但不可以是负值、否则会发生 IllegalArgumentException 异常
java.lang.IllegalArgumentException: Application registered OnBackInvokedCallback cannot have negative priority. Priority: -1
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.