(4.2.44)LoopingViewPager實現迴圈滾動
一、專案地址
只需要在原來的開發人員寫的介面的基礎上新增二個介面就可以了,就是原來的count數量上變為count+2
大神Jake Wharton也是用的這種方式:
You can see a sample usage on ViewPagerIndicator fork (by Jake Wharton)
or on PagerSlidingTabStrip fork (by Andreas Stütz)
- 1
- 2
二、使用
直接替換< android.support.v4.view.ViewPager>為< com.xs.view.LoopViewPager>即可
然後其它的用法和官方的ViewPager的用法一樣
- 1
- 2
- 3
instantiateItem() 方法父元件的處理:通常我們會直接addView,但這裡如果直接這樣寫,則會丟擲IllegalStateException。假設一共有三個view,則當用戶滑到第四個的時候就會觸發這個異常,原因是我們試圖把一個有父元件的View新增到另一個元件
destroyItem() 方法:由於我們在instantiateItem()方法中已經處理了remove的邏輯,因此這裡並不需要處理。
- 實際上,實驗表明這裡如果加上了remove的呼叫,則會出現ViewPager的內容為空的情況。具體原因可以參考上面的ViewPager的原理,比如說當前是最後一個位置4,向右滑動肯定要到0位置的,但是0位置已經被銷燬了,所以View就不存在了
public class MyViewPagerAdapter extends PagerAdapter{
private List<View> mListViews;
public MyViewPagerAdapter(List<View> mListViews) {
this.mListViews = mListViews;//構造方法,引數是我們的頁卡,這樣比較方便。
}
//直接繼承PagerAdapter,至少必須重寫下面的四個方法,否則會報錯
@Override
public void destroyItem (ViewGroup container, int position, Object object) {
//container.removeView(mListViews.get(position));//刪除頁卡
}
@Override
public Object instantiateItem(ViewGroup container, int position){
//這個方法用來例項化頁卡
if(mListViews.get(position).getParent != null){
((ViewGroup)mListViews.get(position).getParent()).removeView(mListViews.get(position));
}
container.addView(mListViews.get(position), 0);//新增頁卡
return mListViews.get(position);
}
@Override
public int getCount() {
return mListViews.size();//返回頁卡的數量
}
@Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0==arg1;//官方提示這樣寫
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
需要注意的是:
- 如果你的PagerAdapter僅用於建立View(也就是不使用FragmentPagerAdapter or FragmentStatePagerAdapter),那麼完全不需要修改相關程式碼
- 如果你想把LoopViewPager用於FragmentPagerAdapter or FragmentStatePagerAdapter,必須在adapter中加入一些自定義的改變
- 在顯示頭尾介面時可能會出現閃爍(譬如你使用了NetworkImageView),你可以通過設定setBoundaryCaching( true ) 來設定快取,這樣頭尾介面就不會每次都載入網路資料,而是使用快取的
三、原理
比如現在有二個View要迴圈切換,顯示的是ONE 和 TWO
| ONE | TWO |
- 1
那如何能讓它迴圈呢。其實這時候是用了一個假象:
- 比如TWO按理再往左邊移動。這時候我們應該要能看到ONE。這樣我們才能感覺這是迴圈,所以我們再TWO的右邊再加一個ONE。
- 同理ONE的介面往右移動也要能看到TWO,所以在ONE的左邊加一個TWO
| TWO | ONE | TWO | ONE |
0 1 2 3
//既然我們最左邊加了一個<0>位置的TWO。我們原先的ONE就變到了<1>位置,所以在剛開始的時候初始化的位置是1而不是0
- 1
- 2
- 3
- 4
- 然後當我們的處於<2>位置的TWO介面朝左邊移動的時候,先是能看到<3>位置的ONE了。這時候在划動過程中先給你一種感覺,以為是看到的是<1>位置的ONE
- 然後當划動結束的時候,通過ViewPager.setCurrentItem(1)方法,將頁面定位到了<1>位置的ONE,這時候你發現,又可以繼續朝右邊移動,然後又能看到<2>位置的TWO了
所以,其實划動時候看到的ONE不是你最剛開始看到的<1>位置的ONE介面。但當切換介面的滑動動作全部結束之後。通過ViewPager.setCurrentItem方法,把介面重新移動回到了最剛開始的<1>位置的ONE。
四、 原碼分析
這裡主要有兩個類LoopPagerAdapterWrapper和LoopViewPager
4.1 LoopPagerAdapterWrapper
其實是類似代理模式的實現,LoopPagerAdapterWrapper持有真正的PagerAdapter,但是重寫了相關方法來實現資料來源的對映關係
public class LoopPagerAdapterWrapper extends PagerAdapter {
//建構函式,既LoopPagerAdapterWrapper裡面的mAdapter就是我們傳入的PagerAdapter
LoopPagerAdapterWrapper(PagerAdapter adapter) {
this.mAdapter = adapter;
}
//在getCount方法我們發現跟我們前面說的一樣,因為要增加頭尾二個介面,所以count這時候要在我們傳入的PagerAdapter的個數基礎上再加上2
@Override
public int getCount() {
return mAdapter.getCount() + 2;
}
//實現對映規則,將其轉換為實際的PageAdapter中的顯示項
@Override
public Object instantiateItem(ViewGroup container, int position) {
int realPosition = (mAdapter instanceof FragmentPagerAdapter || mAdapter instanceof FragmentStatePagerAdapter)
? position
: toRealPosition(position);
if (mBoundaryCaching) {
ToDestroy toDestroy = mToDestroy.get(position);
if (toDestroy != null) {
mToDestroy.remove(position);
return toDestroy.object;
}
}
return mAdapter.instantiateItem(container, realPosition);
}
public int toInnerPosition(int realPosition) {
int position = (realPosition + 1);
return position;
}
int toRealPosition(int position) {
int realCount = getRealCount();
if (realCount == 0)
return 0;
int realPosition = (position-1) % realCount;
if (realPosition < 0)
realPosition += realCount;
return realPosition;
}
//實現對映規則
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
int realFirst = getRealFirstPosition();
int realLast = getRealLastPosition();
int realPosition = (mAdapter instanceof FragmentPagerAdapter || mAdapter instanceof FragmentStatePagerAdapter)
? position
: toRealPosition(position);
if (mBoundaryCaching && (position == realFirst || position == realLast)) {
mToDestroy.put(position, new ToDestroy(container, realPosition,
object));
} else {
mAdapter.destroyItem(container, realPosition, object);
}
}
/*
* 代理模式
*/
@Override
public void finishUpdate(ViewGroup container) {
mAdapter.finishUpdate(container);
}
@Override
public boolean isViewFromObject(View view, Object object) {
return mAdapter.isViewFromObject(view, object);
}
@Override
public void restoreState(Parcelable bundle, ClassLoader classLoader) {
mAdapter.restoreState(bundle, classLoader);
}
@Override
public Parcelable saveState() {
return mAdapter.saveState();
}
@Override
public void startUpdate(ViewGroup container) {
mAdapter.startUpdate(container);
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
mAdapter.setPrimaryItem(container, position, object);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
4.1.1 對映規則
- PageAdapter原始的資料來源
| A | B | C | D |
0 1 2 3
- 1
- 2
- LoopPagerAdapterWrapper中的資料來源是
| D | A | B | C | D | A |
0 1 2 3 4 5
- 1
- 2
- 在LoopPagerAdapterWrapper中需要根據當前下標,推算出實際的PageAdapter對應資料下標,對映關係如下
- realadpater.position=(loopadapter.position-1)%count
0->3 D
1->0 A
2->1 B
3->2 C
4->3 D
5->0 A
- 1
- 2
- 3
- 4
- 5
- 6
4.1.2 快取真實的頭尾介面用於顯示假迴圈
- mBoundaryCaching 標示是否需要快取
如果需要快取,則
- destroyItem中並不實際銷燬,而是放入快取列表
- instantiateItem中並不新建而是直接拿到資料
private int getRealFirstPosition() {
return 1;
}
private int getRealLastPosition() {
return getRealFirstPosition() + getRealCount() - 1;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
...
if (mBoundaryCaching) {
ToDestroy toDestroy = mToDestroy.get(position);
if (toDestroy != null) {
mToDestroy.remove(position);
return toDestroy.object;
}
}
...
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
...
int realFirst = getRealFirstPosition();
int realLast = getRealLastPosition();
if (mBoundaryCaching && (position == realFirst || position == realLast)) {
mToDestroy.put(position, new ToDestroy(container, realPosition,
object));
} else {
mAdapter.destroyItem(container, realPosition, object);
}
...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
4.2 LoopViewPager
通過繼承Viewpager,並設定了一個內部的PagechangeListener,在onPageScrolled的回撥中,發現當內部的pagerAdpater的position滑動到邊界的時候,通過呼叫setCurrentItem,將position又設定到正確的位置
public class LoopViewPager extends ViewPager {
@Override
public void setAdapter(PagerAdapter adapter) {
mAdapter = new LoopPagerAdapterWrapper(adapter);
mAdapter.setBoundaryCaching(mBoundaryCaching);
super.setAdapter(mAdapter);
setCurrentItem(0, false);
}
@Override
public void setCurrentItem(int item) {
if (getCurrentItem() != item) {
setCurrentItem(item, true);
}
}
//setCurrentItem(0)其實應該是setCurrentItem(1)
//因為左邊額外加了一個介面(就是上圖的<0>位置),所以我們的起始時候是從<1>位置開始。所以如果使用者在activity程式碼裡面執行LoopViewPager.setCurrentItem(N, smoothScroll);實際上應該跳到的都是N+1的位置
public void setCurrentItem(int item, boolean smoothScroll) {
int realItem = mAdapter.toInnerPosition(item);
super.setCurrentItem(realItem, smoothScroll);
}
@Override
public int getCurrentItem() {
return mAdapter != null ? mAdapter.toRealPosition(super.getCurrentItem()) : 0;
}
private OnPageChangeListener onPageChangeListener = new OnPageChangeListener() {
private float mPreviousOffset = -1;
private float mPreviousPosition = -1;
@Override
public void onPageSelected(int position) {
int realPosition = mAdapter.toRealPosition(position);
if (mPreviousPosition != realPosition) {
mPreviousPosition = realPosition;
if (mOuterPageChangeListener != null) {
mOuterPageChangeListener.onPageSelected(realPosition);
}
}
}
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
int realPosition = position;
if (mAdapter != null) {
realPosition = mAdapter.toRealPosition(position);
if (positionOffset == 0
&& mPreviousOffset == 0
&& (position == 0 || position == mAdapter.getCount() - 1)) {
setCurrentItem(realPosition, false);
}
}
mPreviousOffset = positionOffset;
if (mOuterPageChangeListener != null) {
if (realPosition != mAdapter.getRealCount() - 1) {
mOuterPageChangeListener.onPageScrolled(realPosition,
positionOffset, positionOffsetPixels);
} else {
if (positionOffset > .5) {
mOuterPageChangeListener.onPageScrolled(0, 0, 0);
} else {
mOuterPageChangeListener.onPageScrolled(realPosition,
0, 0);
}
}
}
}
@Override
public void onPageScrollStateChanged(int state) {
if (mAdapter != null) {
int position = LoopViewPager.super.getCurrentItem();
int realPosition = mAdapter.toRealPosition(position);
if (state == ViewPager.SCROLL_STATE_IDLE
&& (position == 0 || position == mAdapter.getCount() - 1)) {
setCurrentItem(realPosition, false);
}
}
if (mOuterPageChangeListener != null) {
mOuterPageChangeListener.onPageScrollStateChanged(state);
}
}
};
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
唯一不足的地方就是需要監聽pageChangeListener的pageScroll方法,重新設定position的值(具體的方法是呼叫scrollTo進行重繪一遍,比較浪費效能)