本文實現的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