简单的Fragment View与Data的懒加载实现

目标

实现准确监听 ViewPager 使用时,每一个Fragment真实的显示/隐藏事件。
即:在一个 ViewPager 中,其包含的 Fragment 的显示隐藏事件必须一一对应,有一个 Fragment 显示了,那就必定要有一个隐藏,不能多也不能少。最终要达到的效果要像 Activity 的onResume()onPause()一样。

需要明晰几点

  1. 首先众所周知的是,判断 ViewPager 中的 Fragment 真实的显示状态,setUserVisibleHint()方法是很关键的,在单层 ViewPager 中,它的状态,基本上就决定了当前 Fragment 对用户是否可见,然而,它不能监听到 Fragment 的onResume()onPause()事件,所以要二者结合,才能实现我们的目标。
  2. 从其生命周期的执行来看,不管是FragmentPagerAdapter还是FragmentStatePagerAdapter,其创建 Fragment 的时候,必定会走onCreateView()回调,销毁的时候必定会走onDestoryView()回调,所以我们定义的标志变量在onDestroyView()的时候,必定要重置,不然会导致下一次调用onCreateView()方法时的状态混乱。
  3. setUserVisibleHint()方法的调用是早于 Fragment 的声明周期的,也就是说,必须在 Fragment 初始化过后,才能通过这个方法去判断当前 Fragment 的显示状态。换句话说,第一次的setUserVisibleHint()方法的调用是没有意义的,必须通过某种方法过滤掉这次调用,而一个很好的标志就是当前 Fragment 是否已经初始化了。
  4. 不光要实现 Fragment 数据的懒加载,更要实现 View 的懒加载,那么就需要给 Fragment 先添加一个占位的空 View,然后在 Fragment 真实可见的时候再实例化真实的 View,添加给占位的空 View,这样做唯一的坏处就是布局深度多了一层。

思路

由以上分析可知,实际上 Fragment 有三个状态共同来干预,分别是ViewPager指导下的可见状态,也就是 userVisibleHint 变量Fragment实例化状态,用 isInit 来表示Fragment真实View实例化状态,用 isViewCreated 来表示

那么 ViewPager 中的 Fragment 有以下几种状态:

Fragment实际状态userVisibleHintisInitisViewCreated
可见truetruetrue
不可见(已被ViewPager预加载falsetruefalse
不可见(未被ViewPager预加载falsefalsefalse

需要注意的是,即 Fragment 经历过一次实例化后,然后由于 ViewPager 切换到其它页卡,导致 Fragment 被销毁,然后再次回到当前页卡,拿到的 Fragment 在一些情况下会是原有的实例,这时它里面的isInitisViewCreated等标志是已经使用过的二手货,所以在 Fragment 的onDestoryView()方法中,务必要将标志变量重置

还有一点,就是当 Fragment 在父 Activity 回调onResume()onPause()方法时,由于此时 ViewPager 不会回调 Fragment 的setUserVisibleHint()方法,所以需要做一下补充处理。
只需注意两点即可:

  1. 只有当 Fragment 的userVisibleHinttrue时,才需要进行额外处理
  2. 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,会使得逻辑太过复杂。

参考

实现类似微信Viewpager-Fragment的惰性加载,lazy-loading

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值