1. 程式人生 > >(4.2.44)LoopingViewPager實現迴圈滾動

(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

需要注意的是:

  1. 如果你的PagerAdapter僅用於建立View(也就是不使用FragmentPagerAdapter or FragmentStatePagerAdapter),那麼完全不需要修改相關程式碼
  2. 如果你想把LoopViewPager用於FragmentPagerAdapter or FragmentStatePagerAdapter,必須在adapter中加入一些自定義的改變
  3. 在顯示頭尾介面時可能會出現閃爍(譬如你使用了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進行重繪一遍,比較浪費效能)

參考文獻