本文来自 Crocutax 的博客 , 转载请注明出处 http://crocutax.com
ViewPager+Fragment的搭配在日常开发中也比较常见,可用于切换展示不同类别的页面,我们日常所见的咨询、购物、金融、社交等类型的APP都有机会用到这种控件组合.
例如:
ViewPager控件有个特有的预加载机制,即默认情况下当前页面左右两侧的1个页面会被加载,以方便用户滑动切换到相邻的界面时,可以更加顺畅的显示出来.
通过ViewPager的setOffscreenPageLimit(int limit)
可以设置预加载页面数量,当前页面相邻的limit个页面会被预加载进内存.
效果如下:注意看Log输出
所谓的懒加载,其实也就是延迟加载,就是等到该页面的UI展示给用户时,再加载该页面的数据(从网络、数据库等),而不是依靠ViewPager预加载机制提前加载两三个,甚至更多页面的数据.这样可以提高所属Activity的初始化速度,也可以为用户节省流量.而这种懒加载的方式也已经/正在被诸多APP所采用.
但是通过ViewPager方法setOffscreenPageLimit(int limit)
的源码可以发现,ViewPager通过一定的逻辑判断来确保至少会预加载左右两侧相邻的1个页面,也就是说无法通过简单的配置做到懒加载的效果.
ViewPager方法setOffscreenPageLimit(int limit) 相关源码
//默认的缓存页面数量(常量)
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
//缓存页面数量(变量)
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
public void setOffscreenPageLimit(int limit) {
//当我们手动设置的limit数小于默认值1时,limit值会自动被赋值为默认值1(即DEFAULT_OFFSCREEN_PAGES)
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) {
//经过前面的拦截判断后,将limit的值设置给mOffscreenPageLimit,用于
mOffscreenPageLimit = limit;
populate();
}
}
关于变量mOffscreenPageLimit到底是什么.可以从其get方法注释中略见端倪
/**
* 返回空闲状态下的视图层级中,当前页面任何一侧保存的页面数量,默认是1
* Returns the number of pages that will be retained to either side of the
* current page in the view hierarchy in an idle state. Defaults to 1.
*
* @return How many pages will be kept offscreen on either side
* @see #setOffscreenPageLimit(int)
*/
public int getOffscreenPageLimit() {
return mOffscreenPageLimit;
}
至于mOffscreenPageLimit到底是怎么影响ViewPager控件预加载的,暂不追查,因为此次的目的并不是ViewPager运行原理分析.
既然通过ViewPager无法达到我们想要的懒加载效果,那么就得从Fragment自身入手了.
Fragment为我们提供了一个方法setUserVisibleHint(boolean isVisibleToUser)
,其中的参数isVisibleToUser
就是表示该Fragment的UI对于用户是否可见
Fragment的方法 setUserVisibleHint(boolean isVisibleToUser)
/**
* Set a hint to the system about whether this fragment's UI is currently visible
* to the user. This hint defaults to true and is persistent across fragment instance
* state save and restore.
*
* An app may set this to false to indicate that the fragment's UI is
* scrolled out of visibility or is otherwise not directly visible to the user.
* This may be used by the system to prioritize operations such as fragment lifecycle updates
* or loader ordering behavior.
*
* Note: This method may be called outside of the fragment lifecycle.
* and thus has no ordering guarantees with regard to fragment lifecycle method calls.
*
* @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
* false if it is not.
*/
public void setUserVisibleHint(boolean isVisibleToUser) {
if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
&& mFragmentManager != null && isAdded()) {
mFragmentManager.performPendingDeferredStart(this);
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = mState < STARTED && !isVisibleToUser;
}
大意就是通过此方法来设置Fragment的UI对用户是否可见,当该页面对用户可见/不可见时,系统都会回调此方法.
我们可以重写此方法,然后根据回调的isVisibleToUser
参数来进行相关的逻辑判断,以达到懒加载的效果,比如如果isVisibleToUser==true
的话表示当前Fragment对用户可见,此时再去加载页面数据.
由于ViewPager内会装载多个Fragment,而这种懒加载机制对于各个Fragment属于共同操作,因此适合将其抽取到BaseFragment中.
注意
setUserVisibleHint(boolean isVisibleToUser)
方法会多次回调,而且可能会在onCreateView()
方法执行完毕之前回调.如果isVisibleToUser==true,然后进行数据加载和控件数据填充,但是onCreateView()
方法并未执行完毕,此时就会出现NullPointerException空指针异常.
基于以上原因,我们进行数据懒加载的时机需要满足两个条件
onCreateView()
方法执行完毕setUserVisibleHint(boolean isVisibleToUser)
方法返回true所以在BaseFragment中用两个布尔型标记来记录这两个条件的状态.只有同时满足了,才能加载数据
//Fragment的View加载完毕的标记
private boolean isViewCreated;
//Fragment对用户可见的标记
private boolean isUIVisible;
第一步,改变
isViewCreated
标记
当onViewCreated()
方法执行时,表明View已经加载完毕,此时改变isViewCreated
标记为true,并调用lazyLoad()
方法
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
isViewCreated = true;
lazyLoad();
}
第二步,改变
isUIVisible
标记
当setUserVisibleHint(boolean isVisibleToUser)
回调为true时,改变isUIVisible
标记为true,并调用lazyLoad()
方法
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
//isVisibleToUser这个boolean值表示:该Fragment的UI 用户是否可见
if (isVisibleToUser) {
isUIVisible = true;
lazyLoad();
} else {
isUIVisible = false;
}
}
第三步: 在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载
private void lazyLoad() {
//这里进行双重标记判断,是因为setUserVisibleHint会多次回调,并且会在onCreateView执行前回调,必须确保onCreateView加载完毕且页面可见,才加载数据
if (isViewCreated && isUIVisible) {
loadData();
//数据加载完毕,恢复标记,防止重复加载
isViewCreated = false;
isUIVisible = false;
printLog(mTextviewContent+"可见,加载数据");
}
}
第四步:定义抽象方法
loadData()
,具体加载数据的工作,交给子类去完成
protected abstract void loadData();
注意: 数据加载完毕要恢复标记,防止数据重复加载
效果如下:
Github源码