本文实现的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<ItemInfo> 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;
}
}
注意
在某些情况下,由于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