ViewPager+Fragments可以帮助Android开发者方便地实现多个tab页之间的切换。可是在使用中你有没有遇到过一些不是那么美丽的状况呢?比方说,进入页面的前几秒钟特别卡顿?从别的页面切换回来,竟然重新加载了页面?从网上Copy了LazyLoad代码下来,来回切换了几次崩溃了?WTF!!!ViewPager+Fragments用起来并不复杂,不消十分钟你就可以把它引入项目,但是要想获得好的体验,就要多花一点功夫了。这篇文章,我就记录一下自己在开发过程中使用到的优化方法。
LazyLoad
首先我们来复习一下ViewPager的基础知识(使用FragmentAdapter)。ViewPager默认至多预加载2个Fragment(可见页面称为VisibleFragment,可见页面左右各一个InvisibleFragment),至少预加载一个页面(可见页面是第一个,仅在右侧有一个InvisibleFragment)。也就是说,我们第一次进入页面时,ViewPager至少加载了两个Fragment,一个VisibleFragment,一个InvisibleFragment。如果InvisibleFragment有什么耗时操作,就会连累VisibleFragment特别卡顿。
聪明如你肯定会想,这个问题so easy,调用一下ViewPager.setOffscreenPageLimit(0)
,禁止ViewPager预加载不就ok了嘛!naive!
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.setOffscreenPageLimit(int limit)
方法的源码,可以看到预加载页面至少是DEFAULT_OFFSCREEN_PAGES
,这个值是1。
你可能又会想了,InvisibleFragment页面从不可见转为可见,这个状态肯定能够捕捉到吧,在页面变为可见的时候执行耗时操作不就可以了嘛!对头!!!这个方法就是Fragment.setUserVisibleHint(boolean isVisibleToUser),一旦Fragment可见状态发生变化,这个方法就会被调用。所以我们就可以这样写:
/**
* 是否已加载数据
*/
private boolean mLoaded = false;
/**
* 一旦页面可见性发生变化,这个方法就会被调用
* 当页面可见且数据未加载时,加载数据
* @param isVisibleToUser
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser && !mLoaded) {
mLoaded = true;
loadData();
}
}
这个就是所谓的LazyLoad!
ViewPager中的Fragment
学到上面的新技能之后你兴冲冲地去实验,编码>>运行>>不错呀!写博客的小哥没骗我!>>纳尼!崩溃了!这个死骗子,肯定拿假的LazyLoad来骗我,看我不骂死他!看官大爷请息怒!我真没骗你,不信咱们接着往下看!
这个地方的崩溃应该是NullPointerException,提示在View初始化之前即对其进行了操作。看来setUserVisibleHint()方法被调用时,onCreateView()方法还没有被调用。要解决这个问题我们要首先弄清楚Fragment中相关方法被调用的顺序(你可以在自己项目中把方法的调用都打印出来)。
然后我们发现Fragment中相关方法的调用顺序是这个样子的:
setUserVisibleHint()>>onCreateView()>>onActivityCreated()。
setUserVisibleHint()确实是在onCreateView()之前被调用的,这可怎么办呀?表怂呀!这还能难得住你?添加一个全局变量,标识onCreateView()方法是否被调用,如果没有被调用,setUserVisibleHint()不执行加载数据的操作。然后我们还需要在onActivityCreated()里面二次判断是否首次加载。现在代码长这个样子:
/**
* onCreateView()是否被调用
*/
private boolean mOnCreateViewInvoked = false;
/**
* 是否已加载数据
*/
private boolean mLoaded = false;
/**
* 一旦页面可见性发生变化,这个方法就会被调用
* 当onCreateView()已经被调用&&页面可见&&数据未加载时,加载数据
* @param isVisibleToUser
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (mOnCreateViewInvoked && isVisibleToUser && !mLoaded) {
mLoaded = true;
loadData();
}
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mOnCreateViewInvoked = true;
// your code
}
/**
* 二次测试是否首次加载
* @param savedInstanceState
*/
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getUserVisibleHint() && !mLoaded) {
mLoaded = true;
loadData();
}
}
你再试试,现在是不是不崩溃了!我跟你讲了,我写的是真的LazyLoad!
一点小扩展
刷了一会儿你又发现问题了,从别的页面返回之后咋又重新加载数据了?在tab页中,我们可能并不希望,页面每次可见的时候数据都重新加载一遍,而是只在第一次可见的时候加载。看官大爷您这个需求比较简单,只需要一行代码就可以搞定了。我们只需要在ViewPager初始化的时候添加一行设置ViewPager.setOffscreenPageLimit(sizeOfFragments)。这样ViewPager中所有的Framgment都不会被回收了,陪你到天荒地老!
接下来写一篇文章介绍一下两个ViewPager嵌套时,Fragment可见性的判断。
完!