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.
 *
 * 

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.

* *

You should keep this limit low, especially if your pages have complex layouts. * This setting defaults to 1.

* * @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.
 *
 * 

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.

* * @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方法。

[本章完...]

你可能感兴趣的:(ViewPager<第五篇>:预加载和懒加载)