预加载
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_HINT
、BEHAVIOR_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方法。
[本章完...]