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

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

相关推荐
落叶霞枫20 小时前
【uniapp安卓原生语言插件】之华为统一扫码插件【保姆级】开发教程。
android·uni-app·android studio
wodongx12320 小时前
从零开始部署Android环境的Jenkins CI/CD流水线(docker环境,Win系统)
android·docker·jenkins
loitawu20 小时前
Rockchip平台 Android 11 到 Android 16 系统占用内存对比分析
android·ddr·内存占用·rockhip·android内存
用户20187928316720 小时前
浅谈View经GPU渲染绘制的过程
android
AD钙奶-lalala20 小时前
Android中开一个线程是ULT还是KLT
android
冬天vs不冷21 小时前
Java基础(十四):枚举类详解
android·java·python
2501_915106321 天前
上架 App 全流程解析,iOS 应用上架步骤、App Store 审核流程、ipa 文件上传与测试分发经验
android·ios·小程序·https·uni-app·iphone·webview
补补234561 天前
解决漫步者H180Plus(TypeC)耳机连接三星手机后每次播放音频出现滴滴声
android·音视频·三星·typec·edifier·h180plus·滴滴声
木易 士心1 天前
Android Jetpack Compose 从入门到精通
android·android jetpack
alexhilton1 天前
如何构建Android应用:深入探讨原则而非规则
android·kotlin·android jetpack