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

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

前言

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

Fragment可见性解析

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

onResume

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

1
2
3
4
5
6
7
8
9
10
/**
* 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之前。
先来看看源码里的描述。

1
2
3
4
/* @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地址.如果你喜欢的话,不妨点个赞吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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) {
}
}

结语

以上