经验之谈-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-...

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

相关推荐
张可3 小时前
Kotlin 函数式编程思想
android·前端·kotlin
悟乙己3 小时前
如何区分 Context Engineering 与 Prompt Engineering
android·java·prompt
4Forsee4 小时前
【Android】从复用到重绘的控件定制化方式
android
翻滚丷大头鱼4 小时前
android View详解—自定义ViewGroup,流式布局
android·数据结构
胖虎14 小时前
Android入门到实战(八):从发现页到详情页——跳转、传值与RecyclerView多类型布局
android·recyclerview·多类型布局
我想_iwant9 小时前
android集成unity后动态导入 assetsBundle
android·unity·游戏引擎
IAM四十二13 小时前
Android Json 解析你还在用 fastjson 吗?
android·json·fastjson