提供一种Fragment可见性改变的监测方案

4,591 阅读4分钟

原创文章,转载请联系作者

前言

Fragment,这个让人又爱又恨“碎片”。
使用它可以让项目更加轻便--我们可以将功能分割、复用,但其复杂的生命周期和Transaction事务,在极端操作【某些测试人员有一手绝活,三指甚至六指同时触屏乱弹】下会出现一些不可预期的错误--Fragment嵌套Fragment,横竖屏切换等等。
但无论怎样,面对解决问题,才是关键。这篇文章就是针对Fragment监测可见状态改变,提供一种解决方案。

Fragment可见性解析

首先,要说明一下,这里的可见性就是对用户来说看的见。不仅仅是界面位于顶层那种常规情况,而是即便界面上还存在一层透明界面或是对话框,那么依然判定其对用户可见,为visible
接下来会分析在特定交互环境下,Fragment内部被触发的方法。

onResume

Fragment是不能单独存在的,它所在的视图树中,往下追溯,根部一定是一个Activity。在源码中,onResume()方法的描述很有意思。

/**
     * Called when the fragment is visible to the user and actively running.
     * This is generally
     * tied to {@link Activity#onResume() Activity.onResume} of the containing
     * Activity's lifecycle.
     */
    @CallSuper
    public void onResume() {
        mCalled = true;
    }

一般情况下,对用户可见时触发。绑定在依赖的Activity生命周期里

也就是说,一般这个方法,会在可见并且正在活跃时被调用。但说到底,还是个“窝里造”,生命周期完全依赖于父容器----也一定依赖于根Activity
那么不一般的情况下呢?
有这么一个例子,在进入一个Activity界面时,直接调用了beginTransaction().hide(Fragment)方法。那么用户一开始就不会看到这个界面,但生命周期确实也走到了onResume。由此可知,可见性的判断不能只依赖于这一个方法的判断。

onHiddenChanged

这个方法在使用beginTransaction().hide(Fragment)会被调用,而且是在onResume之前。
先来看看源码里的描述。

 /* @param hidden True if the fragment is now hidden, false otherwise.
     */
    public void onHiddenChanged(boolean hidden) {
    }

这个方法会回调出来一个参数,true的时候表示隐藏了,false表示可见。在可见性改变时被调用。
这里要注意一下这个布尔值的定义!

setUserVisibleHint

ViewPager搭配Fragment,也是常见的交互模式了。此时左右滑动时,这个方法会被触发。但有一点要说明一下,当ViewPager初始化时,Fragment相应的生命周期里。setUserVisibleHint方法是走在Fragment的onCreate之前的。

以上几个方法,就是常见的交互下,会被触发的方法了。可见性的监测,主要也依赖于这个方法的相互配合。
这里还需要说明一下,可见性的监测,监测的是
“改变”*。也就是当Fragment被创建出来时,不会触发监测方法,不管它是可见还是不可见的状态。
*

代码实现

在BaseFragment内,提供了一个onVisibleToUserChanged(boolean isVisibleToUser)方法作为内部回调。参数isVisibleToUser如字面所示,True表示可见,false不可见。当你需要在界面不可见,取消网络请求或是释放一些东西,你就可以使用此方案。
代码实现相当简单,就是一连串逻辑代码而已。只是在onResume方法里,需要判断一下是否已经触发了onHiddenChanged或是setuserVisibleHint方法。
代码很短,不到100行。这里直接贴出来。不方便的小可爱们,可以直接去GitHub地址.如果你喜欢的话,不妨点个赞吧。

abstract class BaseFragment : Fragment(){
    lateinit var mRootView: View
    private var isVisibleToUsers = false
    private var isOnCreateView = false
    private var isSetUserVisibleHint = false
    private var isHiddenChanged = false
    private var isFirstResume = false
    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        isOnCreateView = true
        mRootView = LayoutInflater.from(activity).inflate(getResId(), null, false)
        return mRootView
    }

    abstract fun getResId(): Int

    override fun onResume() {
        super.onResume()
        if (!isHiddenChanged && !isSetUserVisibleHint) {
            if (isFirstResume) {
                setVisibleToUser(true)
            }
        }
        if (isSetUserVisibleHint || (!isFirstResume && !isHiddenChanged)) {
            isVisibleToUsers = true
        }
        isFirstResume = true
    }

    override fun onPause() {
        super.onPause()
        isHiddenChanged = false
        isSetUserVisibleHint = false
        setVisibleToUser(false)
    }
    
    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        super.setUserVisibleHint(isVisibleToUser)
        isSetUserVisibleHint = true
        setVisibleToUser(isVisibleToUser)
    }

    override fun onHiddenChanged(hidden: Boolean) {
        super.onHiddenChanged(hidden)
        isHiddenChanged = true
        setVisibleToUser(!hidden)
    }
    
    private fun setVisibleToUser(isVisibleToUser: Boolean) {
        if (!isOnCreateView) {
            return
        }
        if (isVisibleToUser == isVisibleToUsers) {
            return
        }
        isVisibleToUsers = isVisibleToUser
        onVisibleToUserChanged(isVisibleToUsers)
    }

    protected open fun onVisibleToUserChanged(isVisibleToUser: Boolean) {
    }
}

结语

以上