精彩文章免费看

ViewPager<第五篇>:预加载和懒加载

预加载

ViewPager为什么让滑动流畅,默认将左右两个页面加载到了内存,这叫做ViewPager的 预加载 ,但是往往会遇到一些需求,要求每次切换页面都会重新更新当前页面的UI。

那么,为了满足这个需求,有没有什么办法禁止ViewPager预加载的特性呢?

ViewPager有个 setOffscreenPageLimit 方法可以设置预加载页面的个数,方法如下:

viewpager.setOffscreenPageLimit(1);

这个方法的源码如下:

* Set the number of pages that should be retained to either side of the * current page in the view hierarchy in an idle state. Pages beyond this * limit will be recreated from the adapter when needed. * <p>This is offered as an optimization. If you know in advance the number * of pages you will need to support or have lazy-loading mechanisms in place * on your pages, tweaking this setting can have benefits in perceived smoothness * of paging animations and interaction. If you have a small number of pages (3-4) * that you can keep active all at once, less time will be spent in layout for * newly created view subtrees as the user pages back and forth.</p> * <p>You should keep this limit low, especially if your pages have complex layouts. * This setting defaults to 1.</p> * @param limit How many pages will be kept offscreen in an idle state. public void setOffscreenPageLimit(int limit) { if (limit < DEFAULT_OFFSCREEN_PAGES) { Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + DEFAULT_OFFSCREEN_PAGES); limit = DEFAULT_OFFSCREEN_PAGES; if (limit != mOffscreenPageLimit) { mOffscreenPageLimit = limit; populate();

从源码中获取到的信息是:

  • ViewPager预加载页面的数量默认是1;
  • 如果将limit设置成0,那么则强制设置成1;
  • 如果设置成n,则缓存当前页面的左右各n个页面;(n > 0)
  • 所以,通过setOffscreenPageLimit这个方法根本无法禁止ViewPager的预加载。那么,只能从Fragment着手,Fragment有一种懒加载的概念可以满足这个需求。

    以前的方案是这样的,如下:

    【第一步】 Adapter构造方法

    public FragmentStatePagerAdapter(@NonNull FragmentManager fm) {
        this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
    
    @Deprecated
    public FragmentPagerAdapter(@NonNull FragmentManager fm) {
        this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
    

    声明Adapter时使用一个参数的构造方法。

    【第二步】 在自定义Fragment中初始化基本参数

    //是否已经初始化,是否执行了onCreateView
    private boolean isInit = false;
    //是否正在加载
    private boolean isLoad = true;
    

    【第三步】 在onCreateView中处理

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        rootView = inflater.inflate(R.layout.item_base, container, false) ;
        //处理预加载问题,让fragment懒加载
        isInit = true;
        //初始化的时候去加载数据
        loadData();
        return rootView;
    

    【第四步】 重写setUserVisibleHint方法,以及处理

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        loadData();
    

    【第五步】 loadData方法实现

    * 加载数据 private void loadData() { //视图没有初始化 if (!isInit) { return; //判断视图对用户是否可见 if (getUserVisibleHint()) { //懒加载 lazyLoad(); isLoad = true; } else { if (isLoad) { //停止加载 stopLoad();

    【第六步】 lazyLoad方法实现

    * 当视图初始化并对用户可见的时候去真正的加载数据 protected void lazyLoad() { //里面开始对页面进行数据加载 mContent = (String) getArguments().get("content"); TextView textView = (TextView) rootView.findViewById(R.id.tv); textView.setText(mContent);

    【第七步】 stopLoad方法实现

    * 当视图已经对用户不可见并且加载过数据,如果需要在切换到其他页面时停止加载数据,可以覆写此方法 protected void stopLoad() { //让已经在加载过数据并不可见的页面停止加载(例如 视频播放时切换过去不可见时,要让它停止播放)

    【第八步】 销毁时的处理

    @Override
    public void onDestroy() {
        super.onDestroy();
        isInit = false;
        isLoad = false;
    

    在Android 9.0之前,重写Fragment的setUserVisibleHint方法可以得到isVisibleToUser参数,这个参数可以控制UI的显示和隐藏,进而可以实现Fragment的懒加载(延迟加载),但是自从Android9.0之后,AndroidX也随之诞生,setUserVisibleHint方法已被弃用,被FragmentTransaction的setMaxLifecycle替代。所以,Fragment的懒加载有了新的方案。

    setMaxLifecycle定义在FragmentTransaction中,和之前的add、attach、remove、detach、show、hide等方法是并列关系;

    我们看下源码:

    * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is * already above the received state, it will be forced down to the correct state. * <p>The fragment provided must currently be added to the FragmentManager to have it's * Lifecycle state capped, or previously added as part of this transaction. The * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise * an {@link IllegalArgumentException} will be thrown.</p> * @param fragment the fragment to have it's state capped. * @param state the ceiling state for the fragment. * @return the same FragmentTransaction instance @NonNull public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment, @NonNull Lifecycle.State state) { addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state)); return this;

    在AndroidX中,Adapter的构造方法也发生了变化

    @Deprecated
    public FragmentStatePagerAdapter(@NonNull FragmentManager fm) {
        this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
    
    @Deprecated
    public FragmentPagerAdapter(@NonNull FragmentManager fm) {
        this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
    

    我们发现,这两个方法已经被废弃,被两个参数的构造方法替代,如下:

    public FragmentStatePagerAdapter(@NonNull FragmentManager fm,
            @Behavior int behavior) {
        mFragmentManager = fm;
        mBehavior = behavior;
    
    public FragmentPagerAdapter(@NonNull FragmentManager fm,
            @Behavior int behavior) {
        mFragmentManager = fm;
        mBehavior = behavior;
    

    在AndroidX中,FragmentPagerAdapter和FragmentStatePagerAdapter的构造方法的第二个参数是一个Behavior,这个值有两种可能:BEHAVIOR_SET_USER_VISIBLE_HINTBEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT

    如果继续使用带有一个参数的构造方法,Behavior默认取值为BEHAVIOR_SET_USER_VISIBLE_HINT,当然,在AndroidX中,Behavior的取值需要指定为BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT

    假如,我们现在使用的是AndroidX,在FragmentPagerAdapter或FragmentStatePagerAdapter方法中instantiateItem方法,源码如下:

    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        // If we already have this item instantiated, there is nothing
        // to do.  This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        Fragment fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
        while (mFragments.size() <= position) {
            mFragments.add(null);
        fragment.setMenuVisibility(false);
        if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) {
            fragment.setUserVisibleHint(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);
        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
        return fragment;
    

    我们会发现,如果Behavior取值为BEHAVIOR_SET_USER_VISIBLE_HINT,则使用

    fragment.setUserVisibleHint(true|false)
    

    如果Behavior取值为BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,则使用

    mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
    

    所以,在Fragment的UI加载之前,Fragment的生命周期就被指定为Lifecycle.State.STARTED,此时执行Fragment的onStart生命周期。

    当Viewpager切换页面时,会执行到Adapter的setPrimaryItem方法,源码如下:

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

    该方法只告诉我们:当切换到当前Fragment时,执行

    mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
    

    将Fragment的生命周期切换到onResume,执行onResume方法。

    结论:从源码中得到的结论是,Fragment数据的初始化应当在onResume方法中执行,可实现懒加载。

    下面开始代码实现:

    【第一步】 构造方法

    public FragmentPagerAdapter(@NonNull FragmentManager fm,
            @Behavior int behavior) {
        mFragmentManager = fm;
        mBehavior = behavior;
    
    public FragmentStatePagerAdapter(@NonNull FragmentManager fm,
            @Behavior int behavior) {
        mFragmentManager = fm;
        mBehavior = behavior;
    

    需要注意的是,第二个参数behavior取值必须是BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT

    【第二步】 在Fragment中重写onResume方法,加载数据

    @Override
    public void onResume() {
        super.onResume();
        //懒加载
        lazyLoad();
    

    【第三步】 在Fragment中重写onPause方法,处理隐藏页面的逻辑

    @Override
    public void onPause() {
        super.onPause();
        //停止加载
        stopLoad();
    

    上面利用两种方法实现Fragment的懒加载,前者通过setUserVisibleHint来获取Fragment的可见和非可见状态,整理逻辑稍微麻烦了点。后者在AndroidX才可以使用,通过生命周期的方式实现数据的懒加载,当Fragment不可见时执行onPause方法,当Fragment可见时,执行onResume方法。

    [本章完...]