乐趣区

关于android:Fragment-可见性监听方案-完美兼容Android-x

我的公众号 程序员徐公 ,四年中大厂工作教训,回复 黑马 ,支付 Android 学习视频一份,回复 徐公 666,能够取得我精心整顿的简历模板,带你走近大厂。

前言

本篇文章次要提供一种监听 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 操作符
  3. 如果你应用中遇到坑,也欢送随时 call 我,咱们一起解决。如果你有更好的计划,也欢送随时跟我交换

往期文章

面试官系列 - 你真的理解 http 吗

面试官问,https 真的平安吗,能够抓包吗,如何避免抓包吗

java 版剑指 offer 算法集锦

启动优化相干

这几篇文章从 0 到 1,解说 DAG 有向无环图是怎么实现的,以及在 Android 启动优化的利用。

举荐理由:当初挺多文章一谈到启动优化,动不动就聊拓扑构造,这篇文章从数据结构到算法、到设计都给大家说分明了,开源我的项目也有十分强的借鉴意义。

[Android 启动优化(一)– 有向无环图
](https://juejin.cn/post/692679…)

Android 启动优化(二)– 拓扑排序的原理以及解题思路

Android 启动优化(三)– AnchorTask 应用阐明

Android 启动优化(四)- 手把手教你实现 AnchorTask

Android 启动优化(五)- AnchorTask 1.0.0 版本更新了

退出移动版