前言
本篇文章主要提供一种监听 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 操作即可
1 override fun onResume() {
2 info("onResume")
3 super.onResume()
4 onActivityVisibilityChanged(true)
5 }
6
7
8 override fun onPause() {
9 info("onPause")
10 super.onPause()
11 onActivityVisibilityChanged(false)
12 }
Hide 和 Show 操作
Hide 和 show 操作,会促发生命周期的回调,但是 hide 和 show 操作并不会,那么我们可以通过什么方法来监听呢?其实很简单,可以通过 onHiddenChanged 方法
1 /**2 * 调用 fragment show hide 的时候回调用这个方法3 */
4 override fun onHiddenChanged(hidden: Boolean) {
5 super.onHiddenChanged(hidden)
6 checkVisibility(hidden)
7 }
ViewPager 嵌套 Fragment
ViewPager 嵌套 Fragment,这种也是很常见的一种结构。因为 ViewPager 的预加载机制,在 onResume 监听是不准确的。
这时候,我们可以通过 setUserVisibleHint 方法来监听,当方法传入值为true的时候,说明Fragment可见,为false的时候说明Fragment被切走了
1public void setUserVisibleHint(boolean isVisibleToUser)2
有一点需要注意的是,个方法可能先于Fragment的生命周期被调用(在FragmentPagerAdapter中,在Fragment被add之前这个方法就被调用了),所以在这个方法中进行操作之前,可能需要先判断一下生命周期是否执行了。
1 /** 2 * Tab切换时会回调此方法。对于没有Tab的页面,[Fragment.getUserVisibleHint]默认为true。 3 */
4 @Suppress("DEPRECATION")
5 override fun setUserVisibleHint(isVisibleToUser: Boolean) {
6 info("setUserVisibleHint = $isVisibleToUser")
7 super.setUserVisibleHint(isVisibleToUser)
8 checkVisibility(isVisibleToUser)
9 }
10
11 /**12 * 检查可见性是否变化13 *14 * @param expected 可见性期望的值。只有当前值和expected不同,才需要做判断15 */
16 private fun checkVisibility(expected: Boolean) {
17 if (expected == visible) return
18 val parentVisible = if (localParentFragment == null) {
19 parentActivityVisible
20 } else {
21 localParentFragment?.isFragmentVisible() ?: false
22 }
23 val superVisible = super.isVisible()
24 val hintVisible = userVisibleHint
25 val visible = parentVisible && superVisible && hintVisible
26 info(
27 String.format(
28 "==> checkVisibility = %s ( parent = %s, super = %s, hint = %s )",
29 visible, parentVisible, superVisible, hintVisible
30 )
31 )
32 if (visible != this.visible) {
33 this.visible = visible
34 onVisibilityChanged(this.visible)
35 }
36 }
AndroidX 的适配(也是一个坑)
在 AndroidX 当中,FragmentAdapter 和 FragmentStatePagerAdapter 的构造方法,添加一个 behavior 参数实现的。
如果我们指定不同的 behavior,会有不同的表现
当 behavior 为 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 时,
ViewPager 中切换 Fragment,setUserVisibleHint 方法将不再被调用,他会确保 onResume 的正确调用时机当 behavior 为 BEHAVIOR_SET_USER_VISIBLE_HINT,跟之前的方式是一致的,我们可以通过 setUserVisibleHint 结合 fragment 的生命周期来监听
1//FragmentStatePagerAdapter构造方法
2public FragmentStatePagerAdapter(@NonNull FragmentManager fm,
3 @Behavior int behavior) {
4 mFragmentManager = fm;
5 mBehavior = behavior;
6}
7
8//FragmentPagerAdapter构造方法
9public FragmentPagerAdapter(@NonNull FragmentManager fm,
10 @Behavior int behavior) {
11 mFragmentManager = fm;
12 mBehavior = behavior;
13}
14
15@IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
16private @interface Behavior { }
既然是这样,我们就很好适配呢,直接在 onResume 中调用 checkVisibility 方法,判断当前 Fragment 是否可见。
回过头,Behavior 是如何实现的呢?
已 FragmentStatePagerAdapter 为例,我们一起开看看源码
1@SuppressWarnings({"ReferenceEquality", "deprecation"})
2@Override
3public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
4 Fragment fragment = (Fragment)object;
5 if (fragment != mCurrentPrimaryItem) {
6 if (mCurrentPrimaryItem != null) {
7 //当前显示Fragment
8 mCurrentPrimaryItem.setMenuVisibility(false);
9 if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
10 if (mCurTransaction == null) {
11 mCurTransaction = mFragmentManager.beginTransaction();
12 }
13 //最大生命周期设置为STARTED,生命周期回退到onPause
14 mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
15 } else {
16 //可见性设置为false
17 mCurrentPrimaryItem.setUserVisibleHint(false);
18 }
19 }
20
21 //将要显示的Fragment
22 fragment.setMenuVisibility(true);
23 if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
24 if (mCurTransaction == null) {
25 mCurTransaction = mFragmentManager.beginTransaction();
26 }
27 //最大 生命周期设置为RESUMED
28 mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
29 } else {
30 //可见性设置为true
31 fragment.se tUserVisibleHint(true);
32 }
33
34 //赋值
35 mCurrentPrimaryItem = fragment;
36 }
37}
代码比较简单很好理解
当 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 事件呢?
方法肯定是有的。
第一种方法,宿主 Fragment 提供可见性的回调,子 Fragment 监听改回调,有点类似于观察者模式。难点在于子 Fragment 要怎么拿到宿主 Fragment
第二种 case,宿主 Fragment 可见性变化的时候,主动去遍历所有的 子 Fragment,调用 子 Fragment 的相应方法
第一种方法
总体思路是这样的,宿主 Fragment 提供可见性的回调,子 Fragment 监听改回调,有点类似于观察者模式。也有点类似于 Rxjava 中下游持有
第一,我们先定义一个接口
1interface OnFragmentVisibilityChangedListener {
2 fun onFragmentVisibilityChanged(visible: Boolean)
3}
第二步,在 BaseVisibilityFragment 中提供 addOnVisibilityChangedListener 和 removeOnVisibilityChangedListener 方法,这里需要注意的是,我们需要用一个 ArrayList 来保存所有的 listener,因为一个宿主 Fragment 可能有多个子 Fragment。
当 Fragment 可见性变化的时候,会遍历 List 调用 OnFragmentVisibilityChangedListener 的 onFragmentVisibilityChanged 方法
**
1open class BaseVisibilityFragment : Fragment(), View.OnAttachStateChangeListener,
2 OnFragmentVisibilityChangedListener {
3
4
5 private val listeners = ArrayList() 6 7 fun addOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) { 8 listener?.apply { 9 listeners.add(this)10 }11 }1213 fun removeOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {14 listener?.apply {15 listeners.remove(this)16 }17 }1819 private fun checkVisibility(expected: Boolean) {20 if (expected == visible) return21 val parentVisible =22 if (localParentFragment == null) parentActivityVisible23 else localParentFragment?.isFragmentVisible() ?: false24 val superVisible = super.isVisible()25 val hintVisible = userVisibleHint26 val visible = parentVisible && superVisible && hintVisible2728 if (visible != this.visible) {29 this.visible = visible30 listeners.forEach { it ->31 it.onFragmentVisibilityChanged(visible)32 }33 onVisibilityChanged(this.visible)34 }35 }
第三步,在 Fragment attach 的时候,我们通过 getParentFragment 方法,拿到宿主 Fragment,进行监听。这样,当宿主 Fragment 可见性变化的时候,子 Fragment 能感应到。
1override fun onAttach(context: Context) {
2 super.onAttach(context)
3 val parentFragment = parentFragment
4 if (parentFragment != null && parentFragment is BaseVisibilityFragment) {
5 this.localParentFragment = parentFragment
6 info("onAttach, localParentFragment is $localParentFragment")
7 localParentFragment?.addOnVisibilityChangedListener(this)
8 }
9 checkVisibility(true)
10 }
第二种方法
第二种方法,它的实现思路是这样的,宿主 Fragment 生命周期发生变化的时候,遍历子 Fragment,调用相应的方法,通知生命周期发生变化
1//当自己的显示隐藏状态改变时,调用这个方法通知子Fragment
2private void notifyChildHiddenChange(boolean hidden) {
3 if (isDetached() || !isAdded()) {
4 return;
5 }
6 FragmentManager fragmentManager = getChildFragmentManager();
7 List fragments = fragmentManager.getFragments(); 8 if (fragments == null || fragments.isEmpty()) { 9 return;10 }11 for (Fragment fragment : fragments) {12 if (!(fragment instanceof IPareVisibilityObserver)) {13 continue;14 }15 ((IPareVisibilityObserver) fragment).onParentFragmentHiddenChanged(hidden);16 }17}
具体的实现方案,可以看这一篇博客。获取和监听Fragment的可见性
完整代码
1/** 2 * Created by jun xu on 2020/11/26. 3 */
4interface OnFragmentVisibilityChangedListener {
5 fun onFragmentVisibilityChanged(visible: Boolean)
6}
7
8
9/** 10 * Created by jun xu on 2020/11/26. 11 * 12 * 支持以下四种 case 13 * 1. 支持 viewPager 嵌套 fragment,主要是通过 setUserVisibleHint 兼容, 14 * FragmentStatePagerAdapter BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 的 case,因为这时候不会调用 setUserVisibleHint 方法,在 onResume check 可以兼容 15 * 2. 直接 fragment 直接 add, hide 主要是通过 onHiddenChanged 16 * 3. 直接 fragment 直接 replace ,主要是在 onResume 做判断 17 * 4. Fragment 里面用 ViewPager, ViewPager 里面有多个 Fragment 的,通过 setOnVisibilityChangedListener 兼容,前提是一级 Fragment 和 二级 Fragment 都必须继承 BaseVisibilityFragment, 且必须用 FragmentPagerAdapter 或者 FragmentStatePagerAdapter 18 * 项目当中一级 ViewPager adapter 比较特殊,不是 FragmentPagerAdapter,也不是 FragmentStatePagerAdapter,导致这种方式用不了 19 */
20open class BaseVisibilityFragment : Fragment(), View.OnAttachStateChangeListener,
21 OnFragmentVisibilityChangedListener {
22
23
24 companion object {
25 const val TAG = "BaseVisibilityFragment"
26 }
27
28 /** 29 * ParentActivity是否可见 30 */
31 private var parentActivityVisible = false
32
33 /** 34 * 是否可见(Activity处于前台、Tab被选中、Fragment被添加、Fragment没有隐藏、Fragment.View已经Attach) 35 */
36 private var visible = false
37
38 private var localParentFragment: BaseVisibilityFragment? =
39 null
40 private val listeners = ArrayList() 41 42 fun addOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) { 43 listener?.apply { 44 listeners.add(this) 45 } 46 } 47 48 fun removeOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) { 49 listener?.apply { 50 listeners.remove(this) 51 } 52 53 } 54 55 override fun onAttach(context: Context) { 56 info("onAttach") 57 super.onAttach(context) 58 val parentFragment = parentFragment 59 if (parentFragment != null && parentFragment is BaseVisibilityFragment) { 60 this.localParentFragment = parentFragment 61 localParentFragment?.addOnVisibilityChangedListener(this) 62 } 63 checkVisibility(true) 64 } 65 66 override fun onDetach() { 67 info("onDetach") 68 localParentFragment?.removeOnVisibilityChangedListener(this) 69 super.onDetach() 70 checkVisibility(false) 71 localParentFragment = null 72 } 73 74 override fun onResume() { 75 info("onResume") 76 super.onResume() 77 onActivityVisibilityChanged(true) 78 } 79 80 81 override fun onPause() { 82 info("onPause") 83 super.onPause() 84 onActivityVisibilityChanged(false) 85 } 86 87 /** 88 * ParentActivity可见性改变 89 */ 90 protected fun onActivityVisibilityChanged(visible: Boolean) { 91 parentActivityVisible = visible 92 checkVisibility(visible) 93 } 94 95 /** 96 * ParentFragment可见性改变 97 */ 98 override fun onFragmentVisibilityChanged(visible: Boolean) { 99 checkVisibility(visible)100 }101102 override fun onCreate(savedInstanceState: Bundle?) {103 info("onCreate")104 super.onCreate(savedInstanceState)105 }106107 override fun onViewCreated(108 view: View,109 savedInstanceState: Bundle?110 ) {111 super.onViewCreated(view, savedInstanceState)112 // 处理直接 replace 的 case113 view.addOnAttachStateChangeListener(this)114 }115116 /**117 * 调用 fragment add hide 的时候回调用这个方法118 */119 override fun onHiddenChanged(hidden: Boolean) {120 super.onHiddenChanged(hidden)121 checkVisibility(hidden)122 }123124 /**125 * Tab切换时会回调此方法。对于没有Tab的页面,[Fragment.getUserVisibleHint]默认为true。126 */127 override fun setUserVisibleHint(isVisibleToUser: Boolean) {128 info("setUserVisibleHint = $isVisibleToUser")129 super.setUserVisibleHint(isVisibleToUser)130 checkVisibility(isVisibleToUser)131 }132133 override fun onViewAttachedToWindow(v: View?) {134 info("onViewAttachedToWindow")135 checkVisibility(true)136 }137138 override fun onViewDetachedFromWindow(v: View) {139 info("onViewDetachedFromWindow")140 v.removeOnAttachStateChangeListener(this)141 checkVisibility(false)142 }143144 /**145 * 检查可见性是否变化146 *147 * @param expected 可见性期望的值。只有当前值和expected不同,才需要做判断148 */149 private fun checkVisibility(expected: Boolean) {150 if (expected == visible) return151 val parentVisible =152 if (localParentFragment == null) parentActivityVisible153 else localParentFragment?.isFragmentVisible() ?: false154 val superVisible = super.isVisible()155 val hintVisible = userVisibleHint156 val visible = parentVisible && superVisible && hintVisible157 info(158 String.format(159 "==> checkVisibility = %s ( parent = %s, super = %s, hint = %s )",160 visible, parentVisible, superVisible, hintVisible161 )162 )163 if (visible != this.visible) {164 this.visible = visible165 onVisibilityChanged(this.visible)166 }167 }168169 /**170 * 可见性改变171 */172 protected fun onVisibilityChanged(visible: Boolean) {173 info("==> onVisibilityChanged = $visible")174 listeners.forEach {175 it.onFragmentVisibilityChanged(visible)176 }177 }178179 /**180 * 是否可见(Activity处于前台、Tab被选中、Fragment被添加、Fragment没有隐藏、Fragment.View已经Attach)181 */182 fun isFragmentVisible(): Boolean {183 return visible184 }185186 private fun info(s: String) {187 Log.i(TAG, "${this.javaClass.simpleName} ; $s ; this is $this")188 }189190191}
题外话
今年有好长时间没有更新技术博客了,主要是比较忙。拖着拖着,就懒得更新了。
这边博客的技术含量其实不高,主要是适配。
AndroidX FragmentAdapter behavior 的适配
宿主 Fragment 嵌套 Fragment,提供了两种方式解决,一种是自上而下的,一种是自上而下的。借鉴了 Rxjava 的设计思想,下游持有上游的引用,从而控制 Obverable 的回调线程。Obsever 会有下游 Observer 的引用,从而进行一些转换操作,比如 map,FlatMap 操作符
如果你使用中遇到坑,也欢迎随时 call 我,我们一起解决。如果你有更好的方案,也欢迎随时跟我交流
往期文章
面试官:简历上最好不要写Glide,不是问源码那么简单
常见的链表翻转,字节跳动加了个条件,面试者高呼「我太难了」| 图解算法
面试官,怎样实现 Router 框架?
面试 Google, 我失败了 | Google 面经分享
面试官系列- 你真的了解 http 吗
java 版剑指offer集锦
面试官系列 - LeetCode链表知识点&题型总结
RecyclerView缓存机制(咋复用?)
徐公自叙
如果你觉得对你有所帮助的话,可以关注我的公众号 徐公码字(stormjun94),第一时间会在上面更新
