经验之谈-Fragment中监听返回键

常见的方案:

方案一

activity中包含fragment实例的引用,在activity中的onBackPressed()中调用fragment的方法,传递返回键事件。

缺点:增加了activity和fragment之间的耦合性;当有子fragment时,一级级的向下传递也稍显麻烦。

方案二

Google官方提供的onBackPressedDispatcher,该类支持直接在fragment中监听返回键事件。

缺点:

  1. 需要使用isEnable()方法来手动管理对返回键的监听,稍显麻烦.

  2. 每一个fragment都需要监听返回键逻辑,然而监听的顺序决定了,返回键事件的分发顺序,所以事件的分发顺序得不到保证。

  3. fragment再订阅后,即使页面不可见了,也能收到返回键事件。

理想中的方案:

  1. activity和fragment,fragment之间拒绝耦合。
  2. 返回键事件的分发顺序,应该让子fragment先处理,如果不处理再传递给父fragment处理,类似View触摸事件的分发顺序。
  3. 当fragment不可见时,不应该接受到返回键事件。
  4. 交互上要和activity的onBackPressed()类似,并且通过返回值表示要不要消费该事件。

那么要实现上述效果,有两种可行的封装方案:

方案一(继承):

在BaseFragemnt中使用onBackPressedDispatcher监听返回键,然后留出 onFragmentBackPressed()方法给子类实现,通过返回值判断是否消费事件。同时控制不可见的fragment不分发事件。

使用时只需要继承BaseFragemnt类,实现onFragmentBackPressed()就可以。

上面的封装基本能解决需求啦,不过由于使用的是继承,所以也有一些不太完美的地方 比如:

  1. 需要对原有项目的BaseFragemnt做较大的改动,如果有多个BaseFragemnt都要同步修改;

  2. 项目中需要处理返回键的fragment属于少数,所以也就让其他Fragment被迫继承了它们用不着的功能。

所以就有了一个经典的设计原则:多用组合,少用继承

那么再尝试使用组合方式进行封装,把逻辑都封装到一个新类中,哪里需要哪里引用,更为灵活。

方案二(组合):

主要逻辑步骤:

  1. 获取和监听activity中所有的fragment变动。
  2. 把获得的fragment列表转化为fragment树(根据fragment间的父子关系)。
  3. 从树的顶节点(activity)开始向可见的fragment分发返回键事件。

先看下怎么使用

kotlin 复制代码
//MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_backpress_test)
    BackPressDispatcher(this).init() //再activity中调用初始化方法
}
kotlin 复制代码
//再需要处理返回键的fragment中继承接口FragmentBackPressListener,实现方法onFragmentBackPressed
class BackPressTestFragment01: Fragment(), FragmentBackPressListener {

    override fun onFragmentBackPressed(): Boolean {
        Log.e(TAG, "${TAG} 收到了返回键事件")
        return false //返回值代表是否消费事件
    }
}

使用起来挺简单吧!

代码实现也很简单 如下:

kotlin 复制代码
import android.content.Context
import android.util.Log
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager


class BackPressDispatcher(val hostActivity: FragmentActivity) {
    private val TAG = "BackPressDispatcher"
    private val rootPageNode = PageNode(null)
    private val fragmentPageNodeMap = hashMapOf<Fragment, PageNode>()

    fun init(){
        val fragmentManager = hostActivity.supportFragmentManager
        transferFragments2PageNodeTree(getAllFragments(fragmentManager))
        fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
        hostActivity.onBackPressedDispatcher.addCallback(hostActivity, backStackChangeListener)
    }

    private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
        override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
            super.onFragmentAttached(fm, f, context)
            Log.e(TAG, "onFragmentAttached")
            transferFragments2PageNodeTree(arrayListOf(f))
        }

        override fun onFragmentDetached(fm: FragmentManager, f: Fragment) {
            super.onFragmentDetached(fm, f)
            removeFromPageNodeTree(f)
        }
    }

    private val backStackChangeListener = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if(!dispatchBackPressEvent(rootPageNode)){
                isEnabled = false
                hostActivity.onBackPressed()
                isEnabled = true
            }
        }
    }

    private fun dispatchBackPressEvent(pageNode: PageNode): Boolean{
        //不可见的fragment不参与事件分发
        if(pageNode.currFragment?.isResumed == false) return false

        //先派发给子fragment处理
        pageNode.getChildFragments().forEach {
            fragmentPageNodeMap[it]?.let { childPageNode ->
                if(dispatchBackPressEvent(childPageNode)) return true
            }
        }

        //再派发给当前fragment处理
        return (pageNode.currFragment as? FragmentBackPressListener)?.onFragmentBackPressed()?:false
    }


    private fun transferFragments2PageNodeTree(fragments: List<Fragment>){
        Log.e(TAG, "fragments size: ${fragments.size}")
        if(fragments.isEmpty()) return
        for (item in fragments){
            val currPageNode = fragmentPageNodeMap[item]?:PageNode(item)

            item.parentFragment?.let { parentFragment ->
                val parentPageNode = fragmentPageNodeMap[parentFragment]?:PageNode(parentFragment)
                parentPageNode.addChildFragment(item)
                fragmentPageNodeMap.put(parentFragment, parentPageNode)
            }?: run { //没有父Fragment,说明上级就是activity啦
                rootPageNode.addChildFragment(item)
            }

            fragmentPageNodeMap.put(item, currPageNode)
        }

    }


    private fun removeFromPageNodeTree(fragment: Fragment){
        fragment.parentFragment?.let { parentFragment ->
            fragmentPageNodeMap[parentFragment]?.removeChildFragment(fragment)
        }?: run{//没有父Fragment,说明上级就是activity啦
            rootPageNode.removeChildFragment(fragment)
        }
        fragmentPageNodeMap.remove(fragment)
    }

    private fun getAllFragments(fm: FragmentManager): List<Fragment> {
        val result = mutableListOf<Fragment>()
        for (fragment in fm.fragments) {
            result.add(fragment)
            result.addAll(getAllFragments(fragment.childFragmentManager))
        }
        return result
    }

}
kotlin 复制代码
import androidx.fragment.app.Fragment

//一个fragment对应一个这样的节点;activity节点的currFragment == null
class PageNode(var currFragment: Fragment?) {
    private var childFragments = LinkedHashSet<Fragment>()


    fun addChildFragment(fragment: Fragment){
        childFragments.add(fragment)
    }

    fun removeChildFragment(fragment: Fragment){
        childFragments.remove(fragment)
    }

    fun getChildFragments(): LinkedHashSet<Fragment>{
        return childFragments
    }

}
kotlin 复制代码
interface FragmentBackPressListener {
    fun onFragmentBackPressed(): Boolean
}

完整项目地址:github.com/High-Power-...

注意:上述代码未经过充分测试

相关推荐
踢球的打工仔11 小时前
PHP面向对象(7)
android·开发语言·php
安卓理事人11 小时前
安卓socket
android
安卓理事人16 小时前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学18 小时前
Android M3U8视频播放器
android·音视频
q***577418 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober19 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿19 小时前
关于ObjectAnimator
android
zhangphil20 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我21 小时前
从头写一个自己的app
android·前端·flutter
lichong9511 天前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端