OnBackPressedCallback/OnBackInvokedCallback/enableOnBackInvokedCallback身世之谜

参考文档:

提供自定义返回导航

添加对预测性返回手势的支持

添加对预测性返回动画的支持

android:enableOnBackInvokedCallback是什么?

用于选择是否开启预测性返回手势

OnBackPressedCallback是什么?

AndroidX 实现自定义手势导航处理功能 (google推荐方案)AndroidX 1.6.0-alpha05

如果只想迁移onBackPressed至新的api可以不开启enableOnBackInvokedCallback;但是如果想接入预测性返回手势的能力,必须开启enableOnBackInvokedCallback

image.png

问:为什么需要使用AndroidX 1.6.0-alpha05以上呢?

因为之前的版本有bug

image.png

OnBackInvokedCallback是什么?

非AndroidX但是希望添加对预测性返回手势的支持,google提供的降级方案,AndroidX中也能用

image.png

需要配合mainfest属性使用:android:enableOnBackInvokedCallback="true"

application中只对API33(Android 13)及以上生效 activity中只对API34(Android 14)及以上生效

会导致(Activity/dialog/Window等)KeyEvent事件以及onBackPressed方法不回调

image.png

Android 13

Activity

代码示例
class BackDemoActivity : AppCompatActivity() {

    companion object {
        const val TAG = "BackDemoTag"
    }

    private val onBackPressedCallback by lazy(LazyThreadSafetyMode.NONE) {
        object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                Log.d(TAG, "BackDemoActivity:handleOnBackPressed")
                //需要自己实现页面关闭逻辑

                //先关闭onBackPressedCallback可用
                isEnabled = false
                //然后手动执行onBackPressedDispatcher.onBackPressed()
                //最后实际调用的是Activity.onBackPressed(),就实现了页面关闭,finish()也行
                onBackPressedDispatcher.onBackPressed()
            }
        }
    }

    /**
     * 设置onBackPressedCallback是否可用
     */
    fun setOnBackPressedCallbackEnabled(enabled: Boolean) {
        onBackPressedCallback.isEnabled = enabled
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_back_demo)
        //这里传入LifecycleOwner,会自动判断生命周期,不用担心内存泄漏
        //如果不传入LifecycleOwner,需要手动调用停用onBackPressedCallback,否则会造成内存泄漏
        //注意:activity添加多个onBackPressedCallback,只会回调最后一个可用的onBackPressedCallback
        onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
        registerOnBackInvokedCallback()
    }

    override fun onBackPressed() {
        super.onBackPressed()
        Log.d(TAG, "BackDemoActivity:onBackPressed")
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        Log.d(TAG, "BackDemoActivity:onKeyDown: event = $event")
        return super.onKeyDown(keyCode, event)
    }

    override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
        Log.d(TAG, "BackDemoActivity;onKeyUp: event = $event")
        return super.onKeyUp(keyCode, event)
    }

    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        Log.d(TAG, "BackDemoActivity:dispatchKeyEvent: event = $event")
        return super.dispatchKeyEvent(event)
    }

    private var onBackInvokedCallback: OnBackInvokedCallback? = null

    private fun registerOnBackInvokedCallback() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                onBackInvokedCallback = OnBackInvokedCallback {
                    Log.d(TAG, "BackDemoActivity:OnBackInvokedCallback")
                }.also {
                    //priority 0-1000000 数字最大的回调会被调用
                    onBackInvokedDispatcher.registerOnBackInvokedCallback(
                        OnBackInvokedDispatcher.PRIORITY_DEFAULT,
                        it
                    )
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun unregisterOnBackInvokedCallback() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                onBackInvokedCallback?.let {
                    onBackInvokedDispatcher.unregisterOnBackInvokedCallback(it)
                }
            }
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
    }

    override fun onDestroy() {
        //onBackInvokedDispatcher需要移除对应的onBackInvokedCallback,防止内存泄漏
        unregisterOnBackInvokedCallback()
        super.onDestroy()
    }

}
场景一

OnBackPressedCallback:不设置
OnBackInvokedCallback:不设置
android:enableOnBackInvokedCallback="false"
返回:页面关闭,依次回调KeyEvent事件方法以及onBackPressed方法

image.png
场景二

OnBackPressedCallback:设置
OnBackInvokedCallback:不设置/设置
android:enableOnBackInvokedCallback="false"
返回:页面不会关闭,依次回调用KeyEvent事件方法以及handleOnBackPressed、onBackPressed方法

image.png
问:页面为什么不会关闭?是什么影响了页面关闭行为?

因为api中做了判断,OnBackPressedCallback回调的话就不会再执行Activity.onBackPressed().onBackInvoked(),所以页面不会关闭

image.png
image.png
image.png
场景三

OnBackPressedCallback:不设置
OnBackInvokedCallback:不设置
android:enableOnBackInvokedCallback="true"
返回:页面会关闭,之前的所有事件以及方法都不会回调

问:这里打开了enableOnBackInvokedCallback,但是OnBackInvokedCallback没有注册,为什么页面会关闭呢?

因为在Activity中的onCreate中OnBackInvokedCallback默认注册了一个系统的OnBackInvokedCallback回调,执行Activity.onBackInvoked(),所以页面会关闭

image.png
image.png
场景四

OnBackPressedCallback:不设置
OnBackInvokedCallback:设置
android:enableOnBackInvokedCallback="true"
返回:页面不会关闭,只会回调OnBackInvokedCallback方法

image.png
问:页面为什么不会关闭?是什么影响了页面关闭行为?

因为我们在onCreate中注册了自定义OnBackInvokedCallback,按照OnBackInvokedCallback回调优先级,则会调用自定义OnBackInvokedCallback,原先Activity中设置的默认OnBackInvokedCallback将不会回调

注册多个OnBackInvokedCallback,会按照优先级以及注册顺序判断调用哪一个

  • 优先级:回调的优先级决定了调用顺序。优先级高的回调会先于优先级低的回调被调用。

    • 自定义OnBackInvokedCallback的优先级的取值位0-1000000

    • PRIORITY_SYSTEM(值为-1)为系统级的,自定义是无法使用的

    image.png
  • 逆序调用:在同一优先级内,回调会按照注册的逆序被调用。

  • 重复注册:如果同一个回调实例已经被注册过,无论其优先级如何,都会先被注销,然后再重新注册。

image.png
场景五

OnBackPressedCallback:设置
OnBackInvokedCallback:设置
android:enableOnBackInvokedCallback="true"
如果OnBackInvokedCallback的优先级是0
返回:页面不会关闭,只会回调handleOnBackPressed方法

image.png

如果OnBackInvokedCallback的优先级大于0

返回:页面不会关闭,只会回调OnBackInvokedCallback方法

image.png
问:为什么OnBackInvokedCallback优先级是0调用OnBackPressedCallback.handleOnBackPressed,优先级大于0调OnBackInvokedCallback?

ComponentActivity中判断了api33(Android 13)及以上版本,会设置新的OnBackInvokedCallback(优先级0),这个新的OnBackInvokedCallback实际就是调用的OnBackPressedDispatcher.onBackPressed();所以最后如果你注册的OnBackInvokedCallback的优先级大于0,那么就会回调你注册的OnBackInvokedCallback方法;如果等于0就只会调用OnBackPressedCallback.handleOnBackPressed了,当前这个前提是你的自定义OnBackInvokedCallback实在onCreate中或者之前注册的,如果在例如onResume中注册,那还是回调自定义OnBackInvokedCallback;一切调用循序都是遵循优先级+注册逆序

api33(Android 13)及以上版本会设置新的OnBackInvokedCallback(优先级0,onCreate生命周期后注册)

image.png
image.png
image.png
image.png
image.png
小结一下
  • OnBackInvokedCallback调用顺序逻辑

    • 优先级:回调的优先级决定了调用顺序。优先级高的回调会先于优先级低的回调被调用。

    • 逆序调用:在同一优先级内,回调会按照注册的逆序被调用。

    • 重复注册:如果同一个回调实例已经被注册过,无论其优先级如何,都会先被注销,然后再重新注册。

  • OnBackPressedCallback调用顺序逻辑

    • 调用最后一个可用的
  • 在Activity中关闭enableOnBackInvokedCallback,是否设置OnBackPressedCallback不会影响到KeyEvent事件方法以及onBackPressed方法的回调;但是要注意的是会在onBackPressed方法前先回调OnBackPressedCallback.handleOnBackPressed且不再执行Activity.onBackPressed而导致页面不会主动关闭

  • 在Activity中开启enableOnBackInvokedCallback,会导致不再回调KeyEvent事件方法以及onBackPressed方法

    • 如果你设置了OnBackInvokedCallback且优先级是0,会回调OnBackPressedCallback.handleOnBackPressed,因为你设置的OnBackInvokedCallback的优先级跟OnBackPressedCallback中默认实现的OnBackInvokedCallback方法优先级一样,但是OnBackPressedCallback中默认实现的OnBackInvokedCallback方法后注入(在onCreate后)调用注册上去,这个方法最后实际会调用在OnBackPressedCallback.handleOnBackPressed;(注意:如果你设置OnBackInvokedCallback的生命周期不在onCreate中,而在onResume,那么回调用你设置的OnBackInvokedCallback方法)

    • 如果你设置了OnBackInvokedCallback且优先级大于0,会回调OnBackInvokedCallback,因为你设置的优先级高于OnBackPressedCallback中实现的OnBackInvokedCallback方法的优先级

Activity+Fragment

class BackDemoFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //停用activity中的onBackPressedCallback,否则fragment中接受不到handleOnBackPressed事件
        //具体业务的时候不建议直接关闭,可能会影响activity中的业务,应该让activity告诉fragment接受OnBackPressedCallback事件了
        (activity as? BackDemoActivity)?.setOnBackPressedCallbackEnabled(false)
        //这里传入LifecycleOwner,会自动判断生命周期,不用担心内存泄漏
        //如果不传入LifecycleOwner,需要手动调用停用onBackPressedCallback,否则会造成内存泄漏
        //注意:activity添加多个onBackPressedCallback,只会回调最后一个可用的onBackPressedCallback
        activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                Log.d(BackDemoActivity.TAG, "BackDemoFragment:handleOnBackPressed")
            }
        })
        registerOnBackInvokedCallback()
    }

    private var onBackInvokedCallback: OnBackInvokedCallback? = null

    private fun registerOnBackInvokedCallback() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                onBackInvokedCallback = OnBackInvokedCallback {
                    Log.d(BackDemoActivity.TAG, "BackDemoFragment:OnBackInvokedCallback")
                }.also {
                    //priority 0-1000000 数字最大的回调会被调用
                    activity?.onBackInvokedDispatcher?.registerOnBackInvokedCallback(
                        10, it
                    )
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun unregisterOnBackInvokedCallback() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                onBackInvokedCallback?.let {
                    activity?.onBackInvokedDispatcher?.unregisterOnBackInvokedCallback(it)
                }
            }
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
    }

    override fun onDestroy() {
        //onBackInvokedDispatcher需要移除对应的onBackInvokedCallback,防止内存泄漏
        unregisterOnBackInvokedCallback()
        super.onDestroy()
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_back_demo, container, false)
    }

}
场景一

OnBackPressedCallback:activity设置,fragment设置
OnBackInvokedCallback:activity不设置/设置,fragment不设置/设置
android:enableOnBackInvokedCallback="false"
fragment关闭activity:OnBackPressedCallback可用

返回:页面不会关闭,依次回调用KeyEvent事件方法以及fragment.handleOnBackPressed、onBackPressed方法

image.png
场景二

OnBackPressedCallback:activity设置,fragment设置

OnBackInvokedCallback:activity不设置/设置,fragment不设置/设置

android:enableOnBackInvokedCallback="false"

fragment未关闭activity:OnBackPressedCallback可用

返回:页面不会关闭,依次回调用KeyEvent事件方法以及activity.handleOnBackPressed、onBackPressed方法

image.png
问:为什么这里的addCallback跟以往理解不一样,一般情况下add的回调都是添加多少回调多少

这里是因为api里面做了限制,只会回调最后一个isEnable的

image.png
场景三

OnBackPressedCallback:activity不设置,fragment不设置
OnBackInvokedCallback:activity设置,fragment设置 (优先级一样)
android:enableOnBackInvokedCallback="true"
返回:页面不会关闭,只会回调Activity.OnBackInvokedCallback方法

image.png
问:Activity/Fragment都注册了OnBackInvokedCallback(优先级一样),为什么只会掉了Activity.OnBackInvokedCallback的?

跟Activty中OnBackInvokedCallback注册回调原理一致,先判断优先级后判断注册顺序;这里activity跟fragment中注册的OnBackInvokedCallback优先级一样,fragment中会先注册,所以调用fragment.OnBackInvokedCallback

场景四

OnBackPressedCallback:activity设置,fragment不设置/设置
OnBackInvokedCallback:activity设置,fragment设置 (优先级都是1)
android:enableOnBackInvokedCallback="true"
返回:页面不会关闭,只会回调Activity.OnBackPressedCallback.handleOnBackPressed方法

image.png
问:为什么调用的是Activity.handleOnBackPressed?

Activity/Fragment都注册了OnBackInvokedCallback且优先级一样,调用Fragment.OnBackInvokedCallback;但是Activity注册了OnBackPressedCallback且Fragment的OnBackInvokedCallback优先级是0,所以调用Activity.OnBackPressedCallback.handleOnBackPressed

问:如果只把Fragment.OnBackInvokedCallback优先级改成大于0,会怎么样?

会调用Fragment.OnBackInvokedCallback

image.png
问:如果只把Activity.OnBackInvokedCallback优先级改成大于0,会怎么样?

会调用Activity.OnBackInvokedCallback

image.png
小结一下
  • Activity+Fragment时,对于OnBackInvokedCallback/OnBackPressedCallback的回调结果会比较复杂一下,需要深刻理解其调用原理,否则很容易出现预期外的结果

Dialog

class BackDemoDialog(context: Context) : AppCompatDialog(context) {

    init {
        setOnKeyListener { dialog, keyCode, event ->
            Log.d(BackDemoActivity.TAG, "BackDemoDialog;onKeyUp: event = $event")
            false
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                Log.d(BackDemoActivity.TAG, "BackDemoDialog:handleOnBackPressed")
            }
        })

        registerOnBackInvokedCallback()
    }

    override fun onStart() {
        super.onStart()
        registerOnBackInvokedCallback()
    }

    private var onBackInvokedCallback: OnBackInvokedCallback? = null

    private fun registerOnBackInvokedCallback() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                onBackInvokedCallback = OnBackInvokedCallback {
                    Log.d(BackDemoActivity.TAG, "BackDemoDialog:OnBackInvokedCallback")
                }.also {
                    //priority 0-1000000 数字最大的回调会被调用
                    onBackInvokedDispatcher.registerOnBackInvokedCallback(
                        OnBackInvokedDispatcher.PRIORITY_DEFAULT, it
                    )
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun unregisterOnBackInvokedCallback() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                onBackInvokedCallback?.let {
                    onBackInvokedDispatcher.unregisterOnBackInvokedCallback(it)
                }
            }
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
    }

    override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
        Log.d(BackDemoActivity.TAG, "BackDemoDialog;onKeyUp: event = $event")
        return super.onKeyUp(keyCode, event)
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
        Log.d(BackDemoActivity.TAG, "BackDemoDialog;onKeyUp: event = $event")
        return super.onKeyDown(keyCode, event)
    }

    override fun onBackPressed() {
        super.onBackPressed()
        Log.d(BackDemoActivity.TAG, "BackDemoDialog:onBackPressed")
    }

    override fun onStop() {
        //onBackInvokedDispatcher需要移除对应的onBackInvokedCallback,防止内存泄漏
        unregisterOnBackInvokedCallback()
        super.onStop()
    }

}
场景一

OnBackPressedCallback:不设置
OnBackInvokedCallback:不设置
android:enableOnBackInvokedCallback="false"
返回:页面关闭,依次回调KeyEvent事件方法以及onBackPressed方法

image.png
场景二

OnBackPressedCallback:设置
OnBackInvokedCallback:不设置
android:enableOnBackInvokedCallback="false"
返回:页面不关闭,依次回调用KeyEvent事件方法以及handleOnBackPressed、onBackPressed方法

image.png
场景三

OnBackPressedCallback:设置
OnBackInvokedCallback:设置
android:enableOnBackInvokedCallback="true"
返回:页面不关闭,只回调Dialog.OnBackInvokedCallback

image.png
场景四

OnBackPressedCallback:不设置
OnBackInvokedCallback:不设置
android:enableOnBackInvokedCallback="true"
返回:页面关闭,只回调Dialog.onBackPressed

image.png
问:这里开启了enableOnBackInvokedCallback,为什么是调用了Dialog.onBackPressed

跟Activity中一样,系统注册了一个默认的OnBackInvokedCallback,执行的就是Dialog.onBackPressed

image.png
小结一下
  • 基本跟Activity中使用时一致的

Android 8

Android 13 以下enableOnBackInvokedCallback是不生效的,不用考虑OnBackInvokedCallback的影响,设置了也是不执行的

Activity

OnBackPressedCallback:设置

返回:页面不关闭,依次回调KeyEvent事件方法以及handleOnBackPressed,onBackPressed方法

image.png

Activity+Fragment

OnBackPressedCallback:Activity设置/Fragment设置

返回:页面不关闭,依次回调KeyEvent事件方法以及handleOnBackPressed,onBackPressed方法

handleOnBackPressed是调用Activity/Frament的则看哪个是最后一个注册且可用的

image.png

Dialog

OnBackPressedCallback:设置

返回:页面不关闭,依次回调KeyEvent事件方法以及handleOnBackPressed,onBackPressed方法

image.png
小结一下
  • 对于Android13以下影响较小,只需要注意OnBackPressedCallback的回调逻辑即可

总结

  • 关闭enableOnBackInvokedCallback我们需要做什么?

    • 关闭的话可以维持以往api的特性,对KeyEvent事件、onBackPressed方法无影响,只是新特性预见性动画不支持
  • 关闭enableOnBackInvokedCallback的话OnBackPressedCallback可以用么?

    • 只有handleOnBackPressed可用,另外的跟预见性动画相关的方法(handleOnBackProgressed、handleOnBackStarted、handleOnBackCancelled)不会调用;但是需要自己处理页面关闭事件
  • 打开enableOnBackInvokedCallback我们需要做什么?

    • 如果不需要支持新特性预见性动画可以不开启

    • 如果需要支持新特性预见性动画,那么打开后KeyEvent事件、onBackPressed方法将不再回调,必须对相关逻辑代码进行迁移,建议是迁移到OnBackPressedCallback中;不建议使用OnBackInvokedCallback,更不要OnBackPressedCallback+OnBackInvokedCallback混合使用,回调逻辑很复杂,对api理解要求很高,否则无法执行到预期效果

  • 如果没有对开启enableOnBackInvokedCallback做适配,manifest文件中需要显示的关闭enableOnBackInvokedCallback属性,避免三方sdk开启该属性后影响到业务逻辑,例如zendesk

  • onBackPressed已经被官方废弃了,我们需要迁移么?

    • 可以进行迁移,将onBackPressed中的逻辑迁移使用OnBackPressedCallback,但是需要注意的是页面关闭需要业务主动处理
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容