专业的编程技术博客社区

网站首页 > 博客文章 正文

Fragment 可见性监听多种 case 完美兼容

baijin 2024-08-15 00:15:02 博客文章 8 ℃ 0 评论

前言

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

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

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

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

1、一个页面只有一个 fragment 的,使用 replace

2、Hide Show 操作。

3、ViewPager 嵌套 Fragment

4、宿主 Fragment 再嵌套 Fragment,比如 ViewPager 嵌套ViewPager,再嵌套 Fragment

1.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)
}

2.Hide 和 Show 操作

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

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

3.ViewPager 嵌套 Fragmen

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

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

public void setUserVisibleHint(boolean isVisibleToUser)

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

/**
 * 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中切换 FragmentsetUserVisibleHint 方法将不再被调用,他会确保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的状态,使得只有正在显示的 Fragment执行到 onResume() 方法,其他Fragment 只会执行到 onStart() 方法,并且当 Fragment 切换到不显示状态时触发 onPause() 方法。
  • mBehavior 设置为 BEHAVIOR_SET_USER_VISIBLE_HINT 时,会当 frament 可见性发生变化时调用 setUserVisibleHint() ,也就是跟我们上面提到的第一种懒加载实现方式一样。

4宿主 Fragment 再嵌套 Fragment

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

宿主Fragment在生命周期执行的时候会相应的分发到子Fragment中,但是setUserVisibleHintonHiddenChanged却没有进行相应的回调。试想一下,一个ViewPager中有一个FragmentA的tab,而FragmentA中有一个子FragmentBFragmentA被滑走了,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);
    }
}

5完整代码

/**
 * 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")
    }


}

最后,如果你也刚好是从事Android开发相关的工作

在这里我也分享一份收录整理的Android学习PDF+架构视频+面试文档+源码笔记,高级架构技术进阶脑图、Android开发面试专题文档,高级进阶架构文档.都是我经常翻阅学习的资料,里面对近几年的大厂面试高频知识点都有详细的讲解,相信可以有效的帮助大家掌握知识、理解原理。

如果你有需要的话,只需私信我【进阶】即可获取

喜欢本文的话,不妨顺手给我点个赞、评论区留言或者转发支持一下哦~

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表