可循環的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閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容