ViewPager两大弊端优化方案

174 阅读7分钟

对viewpager熟悉的同学都知道,viewpager有2个弊端:一是不能关闭预加载,二是PageAdapter.notifyDataSetChanged()无效问题

其中第一个弊端,不能关闭预加载相信很多人都知道原因了,所以这里不在进行解释,直接将源码放出来估计也能看得懂:

private static final int DEFAULT_OFFSCREEN_PAGES = 1;
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();
    }
}

简单解释下,就是即使我们认为的设置setOffscreenPageLimit(0)也没有用,这个方法就是设置viewpager的预加载数量,当我们设置数量为0时,小于默认数量1,所以设置无效,意味着viewpager的最小预加载数量就是1个。

那么第一个问题的解决方案我知道的就有两种,文末我会给出其中一种我觉得比较好的方案,先看第二个弊端。

另外一个弊端就是调用PageAdapter.notifyDataSetChanged()无法刷新viewpager布局的问题,具体的问题也是可以从ViewPager的源码看出来。

当我们调用adapter.notifyDataSetChanged()的时候,PageAdapter内部会调用ViewPager的dataSetChanged()方法,我们截取该方法的片段:

void dataSetChanged() {
  
    ...
    boolean isUpdating = false;
    for (int i = 0; i < mItems.size(); i++) {
        final ItemInfo ii = mItems.get(i);
        // 从mAdapter获得子布局的位置pos
        final int newPos = mAdapter.getItemPosition(ii.object);

        // 如果子View的pos位置没变,则直接返回,不刷新布局
        if (newPos == PagerAdapter.POSITION_UNCHANGED) {
            continue;
        }

        // 如果newPos等于PagerAdapter.POSITION_NONE
        // 则进入重新布局的逻辑
        if (newPos == PagerAdapter.POSITION_NONE) {
            mItems.remove(i);
            i--;

           ...
    }
}

从上面的代码片段以及简略的注释就可以明白第二个问题的来源,知道了问题的来源,那么解决问题就有了思路,其实我们只需要强制PageAdaper的getItemPosition()返回POSTION_UNCHENGED的就可以了,也就是重写PageAdapter的getItemPosition()方法即可:

// 解决Adapter.notifyDataSetChanged()无效问题
@Override
public int getItemPosition(@NonNull Object object) {
    return POSITION_NONE;
}

第二个弊端解决比较容易,那么如何解决第一个弊端,让ViewPager能够实现懒加载呢?

ViewPager无论如何都是会帮我们预先加载至少一个page,这个设计的初衷是为了能够让我们能提前加载好下一页的数据,这样在page之间滑动的时候能够更加丝滑,视图的滑动不会因为加载数据而卡顿,从而达到一定程度的视觉优化效果,只不过是以牺牲内存为前提的。

试想一下,如果page很多呢,那我们可以设置预加载数量为1,只缓存一页的数据和布局就行,但是如果用户在page间快速滑动呢?page还没缓存及时,就产生了滑动,那么一样还是会造成滑动卡动的情况。

这就有了懒加载的应用场景。

那么要实现viewpager中fragment的懒加载,如何实现呢?

我们希望达到的效果就是能尽量减少内存的消耗,同时在page间滑动不会卡顿。

我们无法避免viewpager的预加载特性,至少都会帮我们预加载1个视图的数据,如果加载的数据量大,滑动的时候还没有加载完成,那么也会出现滑动卡顿的情况。

既然无法避免viewpager的预加载,那么我们就让它预加载我们的视图,只不过这是个空的视图,视图中的数据是等到这个视图真正可见的时候再加载。

这样即使预加载所有的视图(空视图),占用的内存也会很低,同时因为视图已经提前被初始化,在page间滑动的时候也不会造成卡顿,这种思路简直完美。接下来就让我们来实际操作下,实验是检验真理的唯一标准,代码亦如此。

首先新建一个视图: 在这里插入图片描述 下面一个RadioGroup,里面放着三个RadioButton,上面部分则是ViewPager

然后新建一个Activity,并设置引用这个视图(setContentView()),获取控件

在这里插入图片描述

为ViewPager准备PageAdapter,这里我们自定义一个PageAdapter:

在这里插入图片描述

为了清晰的达到我们的实验效果,所以其他逻辑尽量简单。

pageAdapter的关键部分就是getItem(),由这个方法创建我们的fragments,要实现fragment的懒加载,那我们fragment也需要自定义。

先定义一个接口,当我们fragment由不可见变为可见时就会调用这个接口的方法:

public interface OnFirstShowListener {
    void onFirstShow();
}

然后创建一个ShellFragment类,继承自Fragment,然后在onAttach()和setUserVisibleHint()中,按视图是否可见来调用接口的方法: 在这里插入图片描述

在我们的视图不可见的时候,我们只希望让viewpager加载我们的空视图,可见后再加载我们具体的视图和数据,这部分的逻辑就在onFirstShow()中。

我们可以直接在ShellFragment中更新加载后的数据,但这样类的复用率就太低了,一个ShellFragment就对应着一个page,而一个ViewPager一般至少都是2个page以上。

所以我们要让这个Fragment有更高的复用率,同时耦合度降低,可以在当前这个Fragment的基础上在加载一个包含具体业务视图的Fragment。当然,是在ShellFragment可见的时候才加载我们具体的业务Fragment。接着完善我们的ShellFragment吧

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_content, container, false);
}

给ShellFragment设置一个空布局,布局文件中只是一个FramLayout,除了给这个FrameLayout设置ID为root之外,里面没有任何逻辑。

private Fragment mContentFragment;
private Bundle mExtraBundle;
public void setContentFragment(Fragment fragment) {
    if (fragment == null || mContentFragment != null || getActivity() == null) {
        return;
    }

    Bundle srcArgument = fragment.getArguments();
    if (srcArgument == null) {
        srcArgument = new Bundle();
    }
    srcArgument.putAll(mExtraBundle);

    // add之后会调用onCreate和onCreateView()
    if (!fragment.isAdded()) {
        fragment.setArguments(srcArgument);
    }

    mContentFragment = fragment;

    FragmentManager manager = getChildFragmentManager();
    if (manager != null) {
        manager.beginTransaction().replace(R.id.root, mContentFragment).commitAllowingStateLoss();
    }
}

setContentFragment()就是实现懒加载主要的逻辑了,我们将contentFragment传进来,并把参数(如果有的话)设置进去,最后赋值给我们的成员属性mContentFragment,开启ChildFragmentManager事务让这个mContentFragment显示在R.id.root,也就是我们FrameLayout上面就行了。

最终setContentFragment()方法会在OnFirstShowListener接口方法调用。

接下来分别准备3个Fragment,我创建了三个,分别是OneFragment,TwoFragment,ThreeFragment,他们的视图有区分度就行。

fragment准备好了,还剩pageAdapter待完善,那就是上面空出来的getItem()方法,看看这里怎么实现:

在这里插入图片描述

其他未给出的逻辑也类似,只是相应的fragment不是OneFragment,而是TwoFragment,ThreeFragment。这里就省略了。

接下来看看效果:

在这里插入图片描述

这是当页面还没滑动,只显示fragment1视图的时候,当前的视图结构: ![在这里插入图片描述](img-blog.csdnimg.cn/20200807100… =40%x90%) 在这里插入图片描述

因为我们没有给ViewPager设置offsetLimitCount,默认会预加载一个page,所以这时父Fragment中有两个ShellFragment,分别对应fragment1和fragment2的page。

而因为fragment1已经可见,所以会调用onFirstShow()方法,将具体的业务Fragment加载出来,也就是OneFragment。

当我们滑动到fragment2时,视图结构又是怎么样的呢?来看看

在这里插入图片描述

只有显示fragment2时才加载的TwoFragment并显示,可见懒加载已实现。

同时,也在实验的过程中印证了ViewPager.setOffsetLimitCount()的真正意义,就是规定了当前显示页,左右两边会被缓存(预先加载)的页数.

例如,上例设置的setOffsetLimitCount = 1,且当前显示在fragment2,那么就会缓存(预先加载)左右两边各一页,如果当前显示的是fragment3,因为右边已经没有新的一页了,所以只会缓存(预先加载)左边一页fragment2。

兄dei,如果觉得我写的还不错,麻烦帮个忙呗 :-)

  1. 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#^.^#)
  2. 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
  3. 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!

拜托拜托,谢谢各位同学!