009 Fragment- 可见性监听方案

作者: 凤邪摩羯 | 来源:发表于2021-06-15 09:07 被阅读0次

    前言

    本篇文章主要提供一种监听 Fragment 可见性监听的方案,完美多种 case,有兴趣的可以看看。废话不多说,开始进入正文。

    在开发当中, fragment 经常使用到。在很多应用场景中,我们需要监听到 fragment 的显示与隐藏,来进行一些操作。比如,统计页面的停留时长,页面隐藏的时候停止播放视频。

    有些同学可能会说了,这还不容易,直接监听 Fragment 的 onResume,onPause。我只能说,兄弟,too young,too simple。

    下面,让我们一起来实现 fragment 的监听。主要分为几种 case

    • 一个页面只有一个 fragment 的,使用 replace
    • Hide 和 Show 操作
    • ViewPager 嵌套 Fragment
    • 宿主 Fragment 再嵌套 Fragment,比如 ViewPager 嵌套 ViewPager,再嵌套 Fragment

    Replace 操作

    replace 操作这种比较简单,因为他会正常调用 onResume 和 onPause 方法,我们只需要在 onResume 和 onPause 做 check 操作即可

        override fun onResume() {
            info("onResume")
            super.onResume()
            onActivityVisibilityChanged(true)
        }
    
        override fun onPause() {
            info("onPause")
            super.onPause()
            onActivityVisibilityChanged(false)
        }
    复制代码
    

    Hide 和 Show 操作

    Hide 和 show 操作,会促发生命周期的回调,但是 hide 和 show 操作并不会,那么我们可以通过什么方法来监听呢?其实很简单,可以通过 onHiddenChanged 方法

        /**
         * 调用 fragment show hide 的时候回调用这个方法
         */
        override fun onHiddenChanged(hidden: Boolean) {
            super.onHiddenChanged(hidden)
            checkVisibility(hidden)
        }
    复制代码
    

    ViewPager 嵌套 Fragment

    ViewPager 嵌套 Fragment,这种也是很常见的一种结构。因为 ViewPager 的预加载机制,在 onResume 监听是不准确的。

    这时候,我们可以通过 setUserVisibleHint 方法来监听,当方法传入值为true的时候,说明Fragment可见,为false的时候说明Fragment被切走了

    public void setUserVisibleHint(boolean isVisibleToUser)
    复制代码
    

    有一点需要注意的是,个方法可能先于Fragment的生命周期被调用(在FragmentPagerAdapter中,在Fragment被add之前这个方法就被调用了),所以在这个方法中进行操作之前,可能需要先判断一下生命周期是否执行了。

        /**
         * Tab切换时会回调此方法。对于没有Tab的页面,[Fragment.getUserVisibleHint]默认为true。
         */
        @Suppress("DEPRECATION")
        override fun setUserVisibleHint(isVisibleToUser: Boolean) {
            info("setUserVisibleHint = $isVisibleToUser")
            super.setUserVisibleHint(isVisibleToUser)
            checkVisibility(isVisibleToUser)
        }
    
        /**
         * 检查可见性是否变化
         *
         * @param expected 可见性期望的值。只有当前值和expected不同,才需要做判断
         */
        private fun checkVisibility(expected: Boolean) {
            if (expected == visible) return
            val parentVisible = if (localParentFragment == null) {
                parentActivityVisible
            } else {
                localParentFragment?.isFragmentVisible() ?: false
            }
            val superVisible = super.isVisible()
            val hintVisible = userVisibleHint
            val visible = parentVisible && superVisible && hintVisible
            info(
                    String.format(
                            "==> checkVisibility = %s  ( parent = %s, super = %s, hint = %s )",
                            visible, parentVisible, superVisible, hintVisible
                    )
            )
            if (visible != this.visible) {
                this.visible = visible
                onVisibilityChanged(this.visible)
            }
        }
    复制代码
    

    AndroidX 的适配(也是一个坑)

    在 AndroidX 当中,FragmentAdapter 和 FragmentStatePagerAdapter 的构造方法,添加一个 behavior 参数实现的。

    如果我们指定不同的 behavior,会有不同的表现

    1. 当 behavior 为 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 时,ViewPager 中切换 Fragment,setUserVisibleHint 方法将不再被调用,他会确保 onResume 的正确调用时机
    2. 当 behavior 为 BEHAVIOR_SET_USER_VISIBLE_HINT,跟之前的方式是一致的,我们可以通过 setUserVisibleHint 结合 fragment 的生命周期来监听
    //FragmentStatePagerAdapter构造方法
    public FragmentStatePagerAdapter(@NonNull FragmentManager fm,
            @Behavior int behavior) {
        mFragmentManager = fm;
        mBehavior = behavior;
    }
    
    //FragmentPagerAdapter构造方法
    public FragmentPagerAdapter(@NonNull FragmentManager fm,
            @Behavior int behavior) {
        mFragmentManager = fm;
        mBehavior = behavior;
    }
    
    @IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
    private @interface Behavior { }
    复制代码
    

    既然是这样,我们就很好适配呢,直接在 onResume 中调用 checkVisibility 方法,判断当前 Fragment 是否可见。

    回过头,Behavior 是如何实现的呢?

    已 FragmentStatePagerAdapter 为例,我们一起开看看源码

    @SuppressWarnings({"ReferenceEquality", "deprecation"})
    @Override
    public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment)object;
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                //当前显示Fragment
                mCurrentPrimaryItem.setMenuVisibility(false);
                if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                    if (mCurTransaction == null) {
                        mCurTransaction = mFragmentManager.beginTransaction();
                    }
                    //最大生命周期设置为STARTED,生命周期回退到onPause
                    mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
                } else {
                    //可见性设置为false
                    mCurrentPrimaryItem.setUserVisibleHint(false);
                }
            }
    
            //将要显示的Fragment
            fragment.setMenuVisibility(true);
            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                if (mCurTransaction == null) {
                    mCurTransaction = mFragmentManager.beginTransaction();
                }
                //最大 生命周期设置为RESUMED
                mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
            } else {
                //可见性设置为true
                fragment.se tUserVisibleHint(true);
            }
    
            //赋值
            mCurrentPrimaryItem = fragment;
        }
    }
    复制代码
    

    代码比较简单很好理解

    • 当 mBehavior 设置为 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 会通过 setMaxLifecycle 来修改当前Fragment和将要显示的Fragment的状态,使得只有正在显示的 Fragmen t执行到 onResume() 方法,其他 Fragment 只会执行到 onStart() 方法,并且当 Fragment 切换到不显示状态时触发 onPause() 方法。
    • 当 mBehavior 设置为 BEHAVIOR_SET_USER_VISIBLE_HINT 时,会当 frament 可见性发生变化时调用 setUserVisibleHint() ,也就是跟我们上面提到的第一种懒加载实现方式一样。

    更多详情,可以参考这一篇博客Android Fragment + ViewPager的懒加载实现

    宿主 Fragment 再嵌套 Fragment

    这种 case 也是比较常见的,比如 ViewPager 嵌套 ViewPager,再嵌套 Fragment。

    宿主Fragment在生命周期执行的时候会相应的分发到子Fragment中,但是setUserVisibleHint和onHiddenChanged却没有进行相应的回调。试想一下,一个ViewPager中有一个FragmentA的tab,而FragmentA中有一个子FragmentB,FragmentA被滑走了,FragmentB并不能接收到setUserVisibleHint事件,onHiddenChange事件也是一样的。

    那有没有办法监听到宿主的 setUserVisibleHint 和 ,onHiddenChange 事件呢?

    方法肯定是有的。

    1. 第一种方法,宿主 Fragment 提供可见性的回调,子 Fragment 监听改回调,有点类似于观察者模式。难点在于子 Fragment 要怎么拿到宿主 Fragment
    2. 第二种 case,宿主 Fragment 可见性变化的时候,主动去遍历所有的 子 Fragment,调用 子 Fragment 的相应方法

    第一种方法

    总体思路是这样的,宿主 Fragment 提供可见性的回调,子 Fragment 监听改回调,有点类似于观察者模式。也有点类似于 Rxjava 中下游持有

    第一,我们先定义一个接口

    interface OnFragmentVisibilityChangedListener {
        fun onFragmentVisibilityChanged(visible: Boolean)
    }
    复制代码
    

    第二步,在 BaseVisibilityFragment 中提供 addOnVisibilityChangedListener 和 removeOnVisibilityChangedListener 方法,这里需要注意的是,我们需要用一个 ArrayList 来保存所有的 listener,因为一个宿主 Fragment 可能有多个子 Fragment。

    当 Fragment 可见性变化的时候,会遍历 List 调用 OnFragmentVisibilityChangedListener 的 onFragmentVisibilityChanged 方法 **

    open class BaseVisibilityFragment : Fragment(), View.OnAttachStateChangeListener,
            OnFragmentVisibilityChangedListener {
    
        private val listeners = ArrayList<OnFragmentVisibilityChangedListener>()
    
        fun addOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {
            listener?.apply {
                listeners.add(this)
            }
        }
    
        fun removeOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {
            listener?.apply {
                listeners.remove(this)
            }
        }
    
        private fun checkVisibility(expected: Boolean) {
            if (expected == visible) return
            val parentVisible =
                if (localParentFragment == null) parentActivityVisible
                else localParentFragment?.isFragmentVisible() ?: false
            val superVisible = super.isVisible()
            val hintVisible = userVisibleHint
            val visible = parentVisible && superVisible && hintVisible
    
            if (visible != this.visible) {
                this.visible = visible
                listeners.forEach { it ->
                    it.onFragmentVisibilityChanged(visible)
                }
                onVisibilityChanged(this.visible)
            }
        }
    复制代码
    

    第三步,在 Fragment attach 的时候,我们通过 getParentFragment 方法,拿到宿主 Fragment,进行监听。这样,当宿主 Fragment 可见性变化的时候,子 Fragment 能感应到。

    override fun onAttach(context: Context) {
            super.onAttach(context)
            val parentFragment = parentFragment
            if (parentFragment != null && parentFragment is BaseVisibilityFragment) {
                this.localParentFragment = parentFragment
                info("onAttach, localParentFragment is $localParentFragment")
                localParentFragment?.addOnVisibilityChangedListener(this)
            }
            checkVisibility(true)
        }
    复制代码
    

    第二种方法

    第二种方法,它的实现思路是这样的,宿主 Fragment 生命周期发生变化的时候,遍历子 Fragment,调用相应的方法,通知生命周期发生变化

    //当自己的显示隐藏状态改变时,调用这个方法通知子Fragment
    private void notifyChildHiddenChange(boolean hidden) {
        if (isDetached() || !isAdded()) {
            return;
        }
        FragmentManager fragmentManager = getChildFragmentManager();
        List<Fragment> fragments = fragmentManager.getFragments();
        if (fragments == null || fragments.isEmpty()) {
            return;
        }
        for (Fragment fragment : fragments) {
            if (!(fragment instanceof IPareVisibilityObserver)) {
                continue;
            }
            ((IPareVisibilityObserver) fragment).onParentFragmentHiddenChanged(hidden);
        }
    }
    
    复制代码
    

    具体的实现方案,可以看这一篇博客。获取和监听Fragment的可见性

    完整代码

    /**
     * Created by jun xu on 2020/11/26.
     */
    interface OnFragmentVisibilityChangedListener {
        fun onFragmentVisibilityChanged(visible: Boolean)
    }
    
    /**
     * Created by jun xu on 2020/11/26.
     *
     * 支持以下四种 case
     * 1\. 支持 viewPager 嵌套 fragment,主要是通过 setUserVisibleHint 兼容,
     *  FragmentStatePagerAdapter BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 的 case,因为这时候不会调用 setUserVisibleHint 方法,在 onResume check 可以兼容
     * 2\. 直接 fragment 直接 add, hide 主要是通过 onHiddenChanged
     * 3\. 直接 fragment 直接 replace ,主要是在 onResume 做判断
     * 4\. Fragment 里面用 ViewPager, ViewPager 里面有多个 Fragment 的,通过 setOnVisibilityChangedListener 兼容,前提是一级 Fragment 和 二级 Fragment 都必须继承  BaseVisibilityFragment, 且必须用 FragmentPagerAdapter 或者 FragmentStatePagerAdapter
     * 项目当中一级 ViewPager adapter 比较特殊,不是 FragmentPagerAdapter,也不是 FragmentStatePagerAdapter,导致这种方式用不了
     */
    open class BaseVisibilityFragment : Fragment(), View.OnAttachStateChangeListener,
        OnFragmentVisibilityChangedListener {
    
        companion object {
            const val TAG = "BaseVisibilityFragment"
        }
    
        /**
         * ParentActivity是否可见
         */
        private var parentActivityVisible = false
    
        /**
         * 是否可见(Activity处于前台、Tab被选中、Fragment被添加、Fragment没有隐藏、Fragment.View已经Attach)
         */
        private var visible = false
    
        private var localParentFragment: BaseVisibilityFragment? =
            null
        private val listeners = ArrayList<OnFragmentVisibilityChangedListener>()
    
        fun addOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {
            listener?.apply {
                listeners.add(this)
            }
        }
    
        fun removeOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {
            listener?.apply {
                listeners.remove(this)
            }
    
        }
    
        override fun onAttach(context: Context) {
            info("onAttach")
            super.onAttach(context)
            val parentFragment = parentFragment
            if (parentFragment != null && parentFragment is BaseVisibilityFragment) {
                this.localParentFragment = parentFragment
                localParentFragment?.addOnVisibilityChangedListener(this)
            }
            checkVisibility(true)
        }
    
        override fun onDetach() {
            info("onDetach")
            localParentFragment?.removeOnVisibilityChangedListener(this)
            super.onDetach()
            checkVisibility(false)
            localParentFragment = null
        }
    
        override fun onResume() {
            info("onResume")
            super.onResume()
            onActivityVisibilityChanged(true)
        }
    
        override fun onPause() {
            info("onPause")
            super.onPause()
            onActivityVisibilityChanged(false)
        }
    
        /**
         * ParentActivity可见性改变
         */
        protected fun onActivityVisibilityChanged(visible: Boolean) {
            parentActivityVisible = visible
            checkVisibility(visible)
        }
    
        /**
         * ParentFragment可见性改变
         */
        override fun onFragmentVisibilityChanged(visible: Boolean) {
            checkVisibility(visible)
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            info("onCreate")
            super.onCreate(savedInstanceState)
        }
    
        override fun onViewCreated(
            view: View,
            savedInstanceState: Bundle?
        ) {
            super.onViewCreated(view, savedInstanceState)
            // 处理直接 replace 的 case
            view.addOnAttachStateChangeListener(this)
        }
    
        /**
         * 调用 fragment add hide 的时候回调用这个方法
         */
        override fun onHiddenChanged(hidden: Boolean) {
            super.onHiddenChanged(hidden)
            checkVisibility(hidden)
        }
    
        /**
         * Tab切换时会回调此方法。对于没有Tab的页面,[Fragment.getUserVisibleHint]默认为true。
         */
        override fun setUserVisibleHint(isVisibleToUser: Boolean) {
            info("setUserVisibleHint = $isVisibleToUser")
            super.setUserVisibleHint(isVisibleToUser)
            checkVisibility(isVisibleToUser)
        }
    
        override fun onViewAttachedToWindow(v: View?) {
            info("onViewAttachedToWindow")
            checkVisibility(true)
        }
    
        override fun onViewDetachedFromWindow(v: View) {
            info("onViewDetachedFromWindow")
            v.removeOnAttachStateChangeListener(this)
            checkVisibility(false)
        }
    
        /**
         * 检查可见性是否变化
         *
         * @param expected 可见性期望的值。只有当前值和expected不同,才需要做判断
         */
        private fun checkVisibility(expected: Boolean) {
            if (expected == visible) return
            val parentVisible =
                if (localParentFragment == null) parentActivityVisible
                else localParentFragment?.isFragmentVisible() ?: false
            val superVisible = super.isVisible()
            val hintVisible = userVisibleHint
            val visible = parentVisible && superVisible && hintVisible
            info(
                String.format(
                    "==> checkVisibility = %s  ( parent = %s, super = %s, hint = %s )",
                    visible, parentVisible, superVisible, hintVisible
                )
            )
            if (visible != this.visible) {
                this.visible = visible
                onVisibilityChanged(this.visible)
            }
        }
    
        /**
         * 可见性改变
         */
        protected fun onVisibilityChanged(visible: Boolean) {
            info("==> onVisibilityChanged = $visible")
            listeners.forEach {
                it.onFragmentVisibilityChanged(visible)
            }
        }
    
        /**
         * 是否可见(Activity处于前台、Tab被选中、Fragment被添加、Fragment没有隐藏、Fragment.View已经Attach)
         */
        fun isFragmentVisible(): Boolean {
            return visible
        }
    
        private fun info(s: String) {
            Log.i(TAG, "${this.javaClass.simpleName} ; $s ; this is $this")
        }
    
    }
    复制代码
    

    题外话

    这边博客主要是适配。

    1. AndroidX FragmentAdapter behavior 的适配
    2. 宿主 Fragment 嵌套 Fragment,提供了两种方式解决,一种是自上而下的,一种是自上而下的。借鉴了 Rxjava 的设计思想,下游持有上游的引用,从而控制 Obverable 的回调线程。Obsever 会有下游 Observer 的引用,从而进行一些转换操作,比如 map,FlatMap 操作符

    相关文章

      网友评论

        本文标题:009 Fragment- 可见性监听方案

        本文链接:https://www.haomeiwen.com/subject/iivykltx.html