可循环的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<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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容