可循环的ViewPager技术细节

本文实现的CycleViewPager在做轮播图时,实现每个position的页面只实例化一次。
源码地址:https://github.com/RainbleNi/CycleViewPager
做一个可循环的ViewPager原本不难,首先想到的是改写PagerAdapter,在首尾加上一个用于循环的扩展页(首页前面加上和末页相同的扩展页,末页后面加上和首页相同的扩展页)。然后在用户滑到扩展页时,用setCurrentItem直接跳到实际页。
这种方法在实现上非常简单,但是存在如下缺陷:
1 滑到首页和末页时需要实例化非必要的两个扩展页面
2 在进行页面跳转,特别时首末页的循环跳转时,从poplate()中可以分析出,需要回收和实例化大量的页面。
举个例子:
有3个页面进行循环跳转标记为P1,P2,P3,首尾分别加上扩展页P0和P4, 在做P3左滑至P1这个动画的过程中(假设左右缓冲页个数是ViewPager默认的1),首先会回收掉P2,实例化P4,然后无动画跳到实际页P1,实例化P1,P0,P2,再回收掉P3,P4.
一个简单的滑动动作,回收了3个页面,实例化了3个页面。
而实际上折腾了大半圈,内存中存在的还是这三个页面T_T,如果页面复杂的话,对App的体验影响是相当大的。
既然是不合理的,那么问题来了,如何解决这种不必要的反复实例化和销毁。
CycleViewPager 页面的instantiateItem和destroyItem都在populate函数中,populate()的作用就是把需要的页面实例化出来,并且安排他们的位置,销毁不需要的页面,给内存留下空间。poplate中的一套实例化-回收策略在普通序列化的ViewPager中是完美的,通常一个侧滑操作只需要实例化和回收一个页面。而在循环的ViewPager中则不然,例如上面那个例子,三个页面都先被回收又实例化了一遍。
建立页面的缓存机制
destroyItem的时候,并不直接回收,而是将其加入到一个回收列表中

mUnusedItemInfoList.add(mItems.remove(itemIndex));

然后instantiateItem的时候,先从回收列表中寻找对应的itemInfo,找不到再进行真正的实例化。
ItemInfo addNewItem(int position, int index, ...) { ItemInfo ii = getReusedItemInfo(position); .... }

出现问题
原生的populate函数,会从currentItem的左侧开始遍历,先实例化需要的,然后回收不需要的,再从右边开始遍历,实例化需要的,回收不需要的。由于循环ViewPager的特性,例如上面的例子中P4和P1是同一个页面,可以重复利用的,但是由于原生populate的遍历顺序,会先进行P1的实例化,再进行P4的回收,导致重复利用的失败。
应对
在遍历的过程中,只进行已有item的重用,不进行实际的instantiateItem,并对其进行记录。
ItemInfo addNewItem(int position, int index, NeedReLayoutValue value, List infoList) { ItemInfo ii = getReusedItemInfo(position); if (ii != null) { value.mHasReuseItem = true; } else { ii = new ItemInfo(); ii.widthFactor = mAdapter.getPageWidth(position); infoList.add(ii); } ii.position = position; if (index < 0 || index >= mItems.size()) { mItems.add(ii); } else { mItems.add(index, ii); } return ii; }

等遍历结束后,再进行统一的重用和instantiateItem。
private void instanceItem(ItemInfo info, NeedReLayoutValue value) { if (info.object != null) { throw new IllegalStateException("set method require orginal data is empty"); } ItemInfo ii = getReusedItemInfo(info.position); if (ii == null) { info.object = mAdapter.instantiateItem(this, info.position); value.mHasInstanceNew = true; } else { info.object = ii.object; value.mHasReuseItem = true; } }

【可循环的ViewPager技术细节】注意
在某些情况下,由于item的重用,我们只改变了item的位置,没有进行新item的添加,为了让新的位置生效,调用onLayout.如果已经有新的instantiateItem则无需此操作,因为addView后会执行layout。
if (!needRelayout.mHasInstanceNew && needRelayout.mHasReuseItem) { onLayout(false, getLeft(), getTop(), getRight(), getBottom()); }

满足循环的特性
用统一的变量标示在循环的过程中,需要延伸的数量
private static final int CYCLE_POSITION_EXTEND = 2;

从扩展页跳回实际页,为了保证动画效果,我们是在mScrollState == SCROLL_STATE_IDLE时进行跳转的,如果用户一直在滑动,我们没有时机进行跳转就会有问题,所以设置为2,更为靠谱些。
上面这个变量在poplate()的过程中,多处起到了扩展遍历项的作用
//扩展左侧遍历的位置 for (int pos = mCurItem - 1; pos >= 0 - CYCLE_POSITION_EXTEND; pos--) { ... } //扩展右侧遍历的位置 for (int pos = mCurItem + 1; pos < N + CYCLE_POSITION_EXTEND; pos++) { ... }

跳回实际页的操作在setScrollState(int newState)中进行
if (mScrollState == SCROLL_STATE_IDLE && (mCurItem < 0 || mCurItem >= count )) { int newItem = getRealPosition(mCurItem, count); scrollToItem(newItem, false, 0, false); }

在某些情况下,我们的item需要不断的切换显示,例如轮播图。这种情况下,只要内存不紧张,不回收item,是最好的方案,CycleViewPager默认是不回收的。需要回收的话,用此方法设置。
public void setRecycleMode(boolean destroyItemWhenNeeded) { mDestroyItemWhenNeeded = destroyItemWhenNeeded; }

在轮播图的情况下,从末页左滑跳到首页这样的动画用setCurrentItem实现会有歧义,可以使用
// 跳到下一页 public void setNextItem() { setCurrentItem(mCurItem + 1); } // 跳到上一页 public void setPrivItem() { setCurrentItem(mCurItem - 1); }

欢迎提出问题,进行交流
微博:http://weibo.com/nirui666

    推荐阅读