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

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

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

2Zmh5D.gif

a21bf7d0d5f340d8ab09a3ae8ed2ad17.gif


后续将按照如下几个方面去阐述:

  1. 新返回导航的具体影响
  2. 如何确定是否受影响
  3. 适配方案的选择
  4. 适配方案的详述
  5. SDK API 适配方案的深入探讨
  6. 新返回导航支持与否的深入比较和原理分析
  7. 注意和残留事项

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 进行分情况适配。

1672190824402.png

4. 适配方案的详述

4.1 加入新返回导航的支持

Manifest 中针对新返回导航特性引入的属性 enableOnBackInvokedCallback 默认是 false,即默认不支持该特性,支持的话需要声明为 true。

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

实测发现:即便声明成了 false,但如果代码中残存了 13 的新 API(比如 OnBackInvokedCallback)的使用,仍会导致新返回导航发生作用。

也就是说,不支持的话,就不要使用任何新的返回相关 API。

4.2 关闭新返回导航的支持

正如上面所述,按照如下即可关闭对新返回导航的支持:

  1. enableOnBackInvokedCallback 声明为 false(不声明亦可)
  2. 不要使用 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.


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