目标
实现准确监听 ViewPager 使用时,每一个Fragment真实的显示/隐藏事件。
即:在一个 ViewPager 中,其包含的 Fragment 的显示
和隐藏
事件必须一一对应,有一个 Fragment 显示了,那就必定要有一个隐藏,不能多也不能少。最终要达到的效果要像 Activity 的onResume()
和onPause()
一样。
需要明晰几点
- 首先众所周知的是,判断 ViewPager 中的 Fragment 真实的显示状态,
setUserVisibleHint()
方法是很关键的,在单层 ViewPager 中,它的状态,基本上就决定了当前 Fragment 对用户是否可见,然而,它不能监听到 Fragment 的onResume()
和onPause()
事件,所以要二者结合,才能实现我们的目标。 - 从其生命周期的执行来看,不管是
FragmentPagerAdapter
还是FragmentStatePagerAdapter
,其创建 Fragment 的时候,必定会走onCreateView()
回调,销毁的时候必定会走onDestoryView()
回调,所以我们定义的标志变量在onDestroyView()
的时候,必定要重置,不然会导致下一次调用onCreateView()
方法时的状态混乱。 setUserVisibleHint()
方法的调用是早于 Fragment 的声明周期的,也就是说,必须在 Fragment 初始化过后,才能通过这个方法去判断当前 Fragment 的显示状态。换句话说,第一次的setUserVisibleHint()
方法的调用是没有意义的,必须通过某种方法过滤掉这次调用,而一个很好的标志就是当前 Fragment 是否已经初始化了。- 不光要实现 Fragment 数据的懒加载,更要实现 View 的懒加载,那么就需要给 Fragment 先添加一个占位的空 View,然后在 Fragment 真实可见的时候再实例化真实的 View,添加给占位的空 View,这样做唯一的坏处就是布局深度多了一层。
思路
由以上分析可知,实际上 Fragment 有三个状态共同来干预,分别是ViewPager指导下的可见状态,也就是 userVisibleHint 变量
,Fragment实例化状态,用 isInit 来表示
,Fragment真实View实例化状态,用 isViewCreated 来表示
。
那么 ViewPager 中的 Fragment 有以下几种状态:
Fragment实际状态 | userVisibleHint | isInit | isViewCreated |
---|---|---|---|
可见 | true | true | true |
不可见(已被ViewPager预加载 | false | true | false |
不可见(未被ViewPager预加载 | false | false | false |
需要注意的是,即 Fragment 经历过一次实例化后,然后由于 ViewPager 切换到其它页卡,导致 Fragment 被销毁,然后再次回到当前页卡,拿到的 Fragment 在一些情况下会是原有的实例,这时它里面的
isInit
,isViewCreated
等标志是已经使用过的二手货,所以在 Fragment 的onDestoryView()
方法中,务必要将标志变量重置
还有一点,就是当 Fragment 在父 Activity 回调onResume()
和onPause()
方法时,由于此时 ViewPager 不会回调 Fragment 的setUserVisibleHint()
方法,所以需要做一下补充处理。
只需注意两点即可:
- 只有当 Fragment 的
userVisibleHint
为true
时,才需要进行额外处理 - Fragment 的
onResume()
方法,只有当isPause()
调用过一次后,才能添加额外处理,不然在 Fragment 第一次创建时,必定会调用一次onResume()
,这次调用时无意义的,需要过滤掉。
实现代码
abstract class XLazyFragment : Fragment() {
var TAG = ""
get() = "$field{${this.toString().split("{")[1]}"
protected var savedInstanceState: Bundle? = null
private var isInit = false
private var isViewCreated = false
/**用于在父 Activity 影响下的 onResume() 和 onPause() 执行时作出标记*/
private var isPaused = false
protected lateinit var rootView: FrameLayout
protected lateinit var activity: AppCompatActivity
abstract val layoutResId: Int
override fun onAttach(context: Context?) {
super.onAttach(context)
activity = context as AppCompatActivity
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
this.savedInstanceState = savedInstanceState
// 不管什么情况,都必须先给 Fragment 实例化 rootView
rootView = FrameLayout(activity)
rootView.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
// Fragment实例化时,可以根据 userVisibleHint 的值来判断是否可见
if (userVisibleHint) {
// 可见状态对应 primaryPosition 位置的 Fragment,即初始化 ViewPager 后的第一个可见的Fragment
// 此时没什么好说的,直接正常流程初始化,并调用实际的可见状态回调 onRealResume()
Log.d(TAG, "真实view初始化onViewCreated---")
initContentView()
onRealResume()
} else {
// 不可见状态对应非 primaryPosition 位置的 Fragment
// 此时可以什么都不做,也可以给 rootView 添加一个 Loading view,以提升用户体验,类似微信
val view = inflater.inflate(R.layout.app_holder_loading_view, rootView, false)
rootView.addView(view)
}
// 最后将 isInit 置为 true,表明该 Fragment 已经实例化过了
isInit = true
return rootView
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
// 此时的状态为可见,已经初始化,但实际的view还未初始化时,需要进行view的初始化
// 对应的动作为切换到已被 ViewPager 预加载了的,但还未实际可见过的 Fragment
if (isVisibleToUser && isInit && !isViewCreated) {
Log.d(TAG, "真实view初始化userVisibleHint---")
initContentView()
}
// 若该 Fragment 已经走过一次可见流程,在下一次回收之前,显示隐藏就走正常的流程了
if (isInit && isViewCreated) {
if (isVisibleToUser) {
onRealResume()
} else {
onRealPause()
}
}
}
/**
* 实例化真实的布局,并添加给 rootView,在添加之前,需要移除 rootView 可能存在的所有子 View
* 并将 isViewCreated 置为 true
*/
private fun initContentView() {
val view = View.inflate(activity, layoutResId, null)
rootView.removeAllViews()
rootView.addView(view, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
isViewCreated = true
init(savedInstanceState)
}
/**
* 用户的初始化动作
* @param savedInstanceState
*/
abstract fun init(savedInstanceState: Bundle?)
/**
* 在受 Activity 的 onResume() 和 onPause() 影响时,对可见状态回调的补充处理,因为此时不会回调 setUserVisibleHint() 方法
* 必须时当前状态为可见,才能在这里回调真实隐藏方法
*/
@Deprecated("Please use onRealPause()")
override fun onPause() {
super.onPause()
if (userVisibleHint) {
onRealPause()
}
isPaused = true
}
/**
* 在受 Activity 的 onResume() 和 onPause() 影响时,对可见状态回调的补充处理,因为此时不会回调 setUserVisibleHint() 方法
* 必须时当前状态为可见,且pause过的Fragment,才能在这里回调真实可见方法
*/
@Deprecated("Please use onRealResume()")
override fun onResume() {
super.onResume()
if (userVisibleHint && isPaused) {
onRealResume()
}
isPaused = false
}
override fun onDestroyView() {
super.onDestroyView()
if (isViewCreated) {
// 这里定义该方法的意义,是因为有一些东西,比如RxBus的注册和解除注册,要在本类的子类中去定义
// 所以要提供一个与初始化实际View对应的销毁方法,来进行解除注册
onRealDestroyView()
}
// 重置标志
isInit = false
isPaused = false
isViewCreated = false
}
open fun onRealResume() {
Log.d(TAG, "真实可见---")
}
open fun onRealPause() {
Log.d(TAG, "真实隐藏---")
}
open fun onRealDestroyView() {}
}
嵌套状态下的判断
只要牢记一点:ViewPager 初始化的时候,默认会调用位于其 primaryPosition 位置的 Fragment 的setUserVisibleHint(true)
方法,然后就可以结合实际业务和使用情况,针对性的进行定义即可。要弄一个普适性的 LazyFragment,会使得逻辑太过复杂。