Fragment可见性及懒加载终极解决方案

Fragment 有很多种使用方法,官方并没有提供一个统一的 api 来处理 Fragment 的可见性判断和回调,导致在不同的使用场景下需要使用不同的方法来判断 Fragment 的可见性。网上已经有很多讲 Fragment 可见性的文章,但是大部分文章覆盖的使用场景不够全面,有些文章的用法也过时了,因此本人梳理了当前 Fragment 的各种使用场景,提供了一个统一的 api 来处理 Fragment 的可见性。

一般使用场景

在Activity中直接使用

在 xml 文件中声明 Fragment,或者在代码中通过 FragmentTransaction 的 add 或 replace 动态载入 Fragment。这两种情况下都只要监听 Fragment 的 onResume 和 onPause 方法就能判断 Fragment 的可见性。

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

使用show和hide控制显示和隐藏

Google 在 androidx.fragment 1.2.0 中新增了一个 FragmentContainerView,用来替代 FlameLayout 做为 Fragment 的容器,在下文中将使用 FragmentContainerView 作为 Fragment 的容器。

老的用法

通过 FragmentTransaction 的 add 将 Fragment 添加到 FragmentManager 后,Fragment 的生命周期会跟随绑定的 Activity 或父 Fragment 走到 onResume,这个时候,只要所依附的 Activity 或父 Fragment 的生命周期不发生变化,通过 FragmentTransaction 的 show 和 hide 方法控制 Fragment 的显示和隐藏并不会改变 Fragment 的生命周期,这个时候需要监听 onHiddenChanged 判断 Fragment 的可见性。

一般情况下,将 Fragment add 到 FragmentManager 的过程是在 Activity 中的 onCreate 回调中进行的,第一次回调 onHiddenChanged 是在 Fragment 回调 onCreateView 之前。如果需要在 Fragment 第一次可见的时候进行 UI 操作,就会出错,为了避免出错,需要结合 Fragment 的 onResume 和 onPause 判断 Fragment 的可见性。

override fun onHiddenChanged(hidden: Boolean) {
    super.onHiddenChanged(hidden)

    if (hidden) {
        determineFragmentInvisible()
    } else {
        determineFragmentVisible()
    }
}

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

AndroidX用法

调用了 hide 后,接着调用 setMaxLifecycle(fragment, Lifecycle.State.STARTED),Fragment 生命周期会走到 onPause。调用 show 方法后,接着调用 setMaxLifecycle(fragment, Lifecycle.State.RESUMED),Fragment 生命周期会走到 onPause。这样只要监听 Fragment 的 onResume 和 onPause 方法就能判断 Fragment 的可见性。

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

在ViewPager中使用

老的用法

在 support 和 androidx.fragment 1.0.0,通过监听 setUserVisibleHint 判断Fragment 的可见性。如果将 Fragment add 到 FragmentManager 的过程是在 Activity 中的 onCreate 回调中进行的,第一次回调 setUserVisibleHint 也是在 Fragment 回调 onCreateView 之前,也需要结合 Fragment 的 onResume 和 onPause 判断 Fragment 的可见性。

override fun setUserVisibleHint(isVisibleToUser: Boolean) {
    super.setUserVisibleHint(isVisibleToUser)
    
    if (isVisibleToUser) {
        determineFragmentVisible()
    } else {
        determineFragmentInvisible()
    }
}

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

AndroidX用法

谷歌从 androidx.fragment 1.1.0 中开始,对 FragmentPagerAdapter 和 FragmentStatePagerAdapter 进行了调整,支持使用 setMaxLifecycle 控制 Fragment 的生命周期,只需要创建 Adpter 的时候, Behavior 选择 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT

public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;
public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;

public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment)object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            ...
            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                ...
                mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
            } else {
                mCurrentPrimaryItem.setUserVisibleHint(false);
            }
        }
        ...
        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            ...
            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
        } else {
            fragment.setUserVisibleHint(true);
        }
        ...
    }
}

这样只要监听 Fragment 的 onResume 和 onPause 方法就能判断 Fragment 的可见性。

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

在ViewPager2中使用

在 ViewPager2 中使用 Fragment 时,使用的适配器是 FragmentStateAdapter,FragmentStateAdapter 内部使用 FragmentMaxLifecycleEnforcer ,FragmentMaxLifecycleEnforcer 也是通过 setMaxLifecycle 控制 Fragment 的生命周期

class FragmentStateAdapter {

    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        ...
        mFragmentMaxLifecycleEnforcer = new FragmentMaxLifecycleEnforcer();
        ...
    }

    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
        ...
        mFragmentMaxLifecycleEnforcer = null;
    }

    class FragmentMaxLifecycleEnforcer {

        void updateFragmentMaxLifecycle(boolean dataSetChanged) {
            ...
            for (int ix = 0; ix < mFragments.size(); ix++) {
                ...
                if (itemId != mPrimaryItemId) {
                    transaction.setMaxLifecycle(fragment, STARTED);
                } else {
                    toResume = fragment; // itemId map key, so only one can match the predicate
                }
                ...
            }
            if (toResume != null) { // in case the Fragment wasn't added yet
                transaction.setMaxLifecycle(toResume, RESUMED);
            }
            ...
        }
    }
}

这样只要监听 Fragment 的 onResume 和 onPause 方法就能判断 Fragment 的可见性。

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

具体实现

IFragmentVisibility 中定义 Fragment 可见性相关方法:

interface IFragmentVisibility {

    /**
     * Fragment可见时调用。
     */
    fun onVisible() {}

    /**
     * Fragment不可见时调用。
     */
    fun onInvisible() {}

    /**
     * Fragment第一次可见时调用。
     */
    fun onVisibleFirst() {}

    /**
     * Fragment可见时(第一次除外)调用。
     */
    fun onVisibleExceptFirst() {}

    /**
     * Fragment当前是否对用户可见
     */
    fun isVisibleToUser(): Boolean
}

Fragment可见

Fragment 可见受到几个因素影响:Fragment 是否处于 RESUMED 状态、Fragment 是否显示、Fragment Hint 是否对用户可见,判断Fragment可见性可能会被连续调用多次,如果当前已经对用户可见,则不进行判断可见性。

// Fragment当前是否对用户可见。
private var mIsFragmentVisible = false

// Fragment当前是否是第一次对用户可见。
private var mIsFragmentVisibleFirst = true

private fun determineFragmentVisible() {
    if (isResumed && !isHidden && userVisibleHint && !mIsFragmentVisible) {
        mIsFragmentVisible = true
        onVisible()
        if (mIsFragmentVisibleFirst) {
            mIsFragmentVisibleFirst = false
            onVisibleFirst()
        } else {
            onVisibleExceptFirst()
        }
    }
}

Fragment不可见

当 Fragment 处于可见状态,调用一次 determineFragmentInvisible 方法,Fragment 就变成不可见了。

private fun determineFragmentInvisible() {
    if (mIsFragmentVisible) {
        mIsFragmentVisible = false
        onInvisible()
    }
}

Fragment嵌套

老的用法

从日志中可以看到,Fragment-1 和 Fragment-1-1 处于可见状态,但是奇怪的是 Fragment-2-1 也处于可见状态,这不符合逻辑,判断可见性逻辑还有待优化的地方。

分析日志可知,所有的 Fragment 生命周期都走到了onResume,但是 Fragment-2、Fragment-1-2、Fragment-2-2 因为 isHidden = true,判断出是不可见状态。Fragment-2-1 是 isHidden = false,但是 Fragment-2 是 isHidden = true,从逻辑上父 Fragment 不可见,子 Fragment 也应该不可见。所以在判断 Fragment 是否可见的时候,还要考虑父 Fragment 是否可见(如果存在父 Fragment)。

当从 Fragment-1 切换到 Fragment-2 后,可以看到,Fragment-1 不可见,Fragment-2 可见,但是本应该不可见的 Fragment-1-1 还是可见,本应该可见的 Fragment-2-1 还是不可见,说明判断可见性逻辑还有待优化的地方。

从 Fragment-1 切换到 Fragment-2,这两者的 onHiddenChanged 被调用了,所以它们的可见性发生了变化。Fragment-1-1 和 Fragment-2-1 没有任何操作,但是它们的可见性也应该随着父Fragment 可见性发生变化而变化,所以应该在父 Fragment 可见性变化的时候重新判断一次子 Fragment 的可见性。

AndroidX用法

全部使用 setMaxLifecycle 控制 Fragment 生命周期,可以看到 Fragment 的可见性判断是正确的。

从 Fragment-1 切换到 Fragment-2,可见性判断还是正确的。

子 Fragment 的生命周期会根据所绑定的 Activity 或父 Fragment 的生命周期变化而变化,setMaxLifecycle 改变了父 Fragment 的生命周期,子 Fragment 的生命周期自然就跟着变化了。所以,仅监听 Fragment 的 onResume 和 onPause 就能判断 Fragment 的可见性,不需要调整判断逻辑。

具体实现

在 determineFragmentVisible 中增加判断父 Fragment 是否可见的代码:

private fun determineFragmentVisible() {
    val parent = parentFragment
    if (parent != null && parent is VisibilityFragment) {
        if (!parent.isVisibleToUser()) {
            // 父Fragment不可见,子Fragment也一定不可见
            return
        }
    }
    ...
}

在 determineFragmentVisible 和 determineFragmentInvisible 增加判断子 Fragment 的可见性代码:

private fun determineFragmentVisible() {
    ...
    if (isResumed && !isHidden && userVisibleHint && !mIsFragmentVisible) {
        ...
        determineChildFragmentVisible()
    }
}

private fun determineFragmentInvisible() {
    if (mIsFragmentVisible) {
        ...
        determineChildFragmentInvisible()
    }
}

private fun determineChildFragmentVisible() {
    childFragmentManager.fragments.forEach {
        if (it is VisibilityFragment) {
            it.determineFragmentVisible()
        }
    }
}

private fun determineChildFragmentInvisible() {
    childFragmentManager.fragments.forEach {
        if (it is VisibilityFragment) {
            it.determineFragmentInvisible()
        }
    }
}

懒加载

在实现了上述功能后,对于需要懒加载功能的 Fragment,只需要重写 onVisibleFirst,在里面加载数据就可以了。

总结

对于全部使用 setMaxLifecycle 控制 Fragment 生命周期的代码,Fragment 的可见性判断相对比较简单,只要监听 Fragment 的 onResume 和 onPause 方法就能判断 Fragment 的可见性。

对于老的用法或者老的用法和 setMaxLifecycle 混用的代码,Fragment 可见性判断不仅要考虑使用方式,也要考虑父 Fragment 的可见性,同时自身可见性改变的时候,也要主动调用子 Fragment 判断可见性的代码。

项目地址

fragment-visibility,觉得用起来很爽的,请不要吝啬你的 Star !

参考

如何判断Fragment是否对用户可见

Fragment新功能,setMaxLifecycle了解一下

Androidx 下 Fragment 懒加载的新实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容