(LoopingViewPager)可循環(huán)的ViewPager實現(xiàn)及詳細(xì)分析

前提:前幾天無聊,下了個《讀者》app,然后正好使用的過程中發(fā)現(xiàn)發(fā)現(xiàn)從文字列表點擊進(jìn)去后可以查看具體的文章內(nèi)容,然后再文章內(nèi)容中還可以左劃右劃來實現(xiàn)文章的切換,然后想到應(yīng)該是ViewPager+fragment來實現(xiàn)的,前面用過這個ViewPager,但是沒有好好看過,所以今天周六抽空好好研究了下關(guān)于ViewPager這一塊,特別是循環(huán),也就是當(dāng)你劃到最后一個界面,再劃的時候,可以回到第一個界面。

-----------------------------基礎(chǔ)使用知識講解分割君---------------------------------------

關(guān)于ViewPager的基礎(chǔ)講解的內(nèi)容我這邊就引用其他大神的文章里面的內(nèi)容。
在此感謝簡書的Carson_Ho

-------------------------------ViewPager循環(huán)切換分割線------------------------------

好了,現(xiàn)在開始我是怎么去學(xué)循環(huán)切換的。

首先第一步:百度搜索(好吧,我沒用谷歌,不是找bug的解決方法。百度夠了)。

網(wǎng)上我看到有一種是設(shè)置PagerAdapter里面的getCount設(shè)置為Integer.MAX_VALUE。然后再設(shè)置其他內(nèi)容。其實是讓界面類似接近(無限多),反正客戶不會吃飽沒事干不停往后面劃動。但我覺得這樣不是特別好。所以沒使用。
大家也可以看下實現(xiàn)方式,不過不管怎么樣。能實現(xiàn)就都是棒棒噠??
感謝簡書的violinlin
Banner的封裝--實現(xiàn)ViewPager的循環(huán)輪播效果

我百度過后,在github中看到一個關(guān)于ViewPager可循環(huán)切換的別人封裝好的自定義ViewPager:LoopingViewPager。
https://github.com/imbryk/LoopingViewPager
只需要在原來的開發(fā)人員寫的界面的基礎(chǔ)上添加二個界面就可以了,就是原來的count數(shù)量上變?yōu)閏ount+2。

拉到網(wǎng)頁最下面寫著別人的例子里面也用到的這個的LoopingViewPager的鏈接,啥 Jake Wharton都用了?! 那我就沒猶豫,馬上嘗試體驗下這個了。

按照github中作者提到的,當(dāng)前循環(huán)分為二種情況,一種是用在ViewPager里面裝的是View,然后View來循環(huán),還有一種是ViewPager里面是Fragment,然后Fragment的循環(huán)

Paste_Image.png

在研究前我們要先學(xué)會使用

里面一共就二個自定義文件:LoopViewPager.java和LoopPagerAdapterWrapper.java,分別繼承了ViewPager.java 和PagerAdapter.java

我們先講ViewPager里面是View的循環(huán)

View循環(huán)特別方便。我們需要把我們Activity中的
<android.support.v4.view.ViewPager>標(biāo)簽替換成<LoopViewPager>標(biāo)簽。

布局ViewPager替換

比如我現(xiàn)在是二個View的切換,二個View分別是加載下圖的那個布局

第一個View 的布局
第二個View的布局

這個我們自定義的繼承PagerAdapter的ViewAdapter類,如果是按照
Android開發(fā):ViewPage詳細(xì)使用教程里面的教程寫的。那instantiateItem方法和destoryItem方法先改成我下面圖片那樣。不然等會循環(huán)的時候會報錯。原因后面我會解釋

ViewAdapter.java

activity代碼

然后可以看到效果:

Paste_Image.png
Paste_Image.png

然后手指繼續(xù)往左滑動,會送Fragment_TWO 又回到了Fragment_ONE的界面。

SO ------ WHY ?

我先來講解一下大概思路。這樣大家后面看講解的時候就會更容易理解,
比如現(xiàn)在有二個View要循環(huán)切換,顯示的是ONE 和 TWO

ONE和TWO二個界面

那如何能讓它循環(huán)呢。其實這時候是用了一個假象。
比如TWO按理再往左邊移動。這時候我們應(yīng)該要能看到ONE。這樣我們才能感覺這是循環(huán),所以我們再TWO的右邊再加一個ONE。同理ONE的界面往右移動也要能看到TWO,所以在ONE的左邊加一個TWO。

變?yōu)樗膫€界面

既然我們最左邊加了一個<0>位置的TWO。我們原先的ONE就變到了<1>位置,所以在剛開始的時候初始化的位置是1而不是0.

然后當(dāng)我們的處于<2>位置的TWO界面朝左邊移動的時候,先是能看到<3>位置的ONE了。這時候在劃動過程中先給你一種感覺,以為是看到的是<1>位置的ONE,然后當(dāng)劃動結(jié)束的時候,通過ViewPager.setCurrentItem(1)方法,將頁面定位到了<1>位置的ONE,這時候你發(fā)現(xiàn),又可以繼續(xù)朝右邊移動,然后又能看到<2>位置的TWO了。

所以其實劃動時候看到的ONE不是你最剛開始看到的<1>位置的ONE界面。但當(dāng)切換界面的動作全部結(jié)束之后。通過ViewPager.setCurrentItem方法,把界面重新移動回到了最剛開始的<1>位置的ONE。

------------------------------------源碼分析分割線----------------------------------------

因為我們只是把的v4包下的ViewPager替換成了LoopViewPager。所以我們先看LoopViewPager在執(zhí)行setAdapter()方法之后到底做了什么處理。

@Override
    public void setAdapter(PagerAdapter adapter) {
        mAdapter = new LoopPagerAdapterWrapper(adapter);//第一步
        mAdapter.setBoundaryCaching(mBoundaryCaching);//第二步
        super.setAdapter(mAdapter);//第三步
        setCurrentItem(0, false);//第四步
    }

我們一步步來分析:

第一步:

把我們傳入的PagerAdapter再傳入到自定義的LoopPagerAdapterWrapper中,進(jìn)行封裝,因為LoopPagerAdapterWrapper本身也是繼承PagerAdapter的。所以等會真正給ViewPager設(shè)置adapter的時候已經(jīng)變?yōu)榱私?jīng)過LoopPagerAdapterWrapper封裝過的adapter了。具體封裝等會再分析。

第二步:
/**
     * If set to true, the boundary views (i.e. first and last) will never be destroyed
     * This may help to prevent "blinking" of some views 
     * 
     * @param flag
     */
    public void setBoundaryCaching(boolean flag) {
        mBoundaryCaching = flag;
        if (mAdapter != null) {
            mAdapter.setBoundaryCaching(flag);
        }
    }

主要是用來設(shè)置是否第一個和最后一個view要緩存,不去銷毀。而第一個和最后一個你懂得。就是我們?yōu)榱搜h(huán)效果而寫的那二個界面。因為跟循環(huán)的原理關(guān)系不是很大。所以這里就不多介紹了。

第三步:

把我們上面經(jīng)過LoopPagerAdapterWrapper封裝過的adapter。賦予給ViewPager。

第四步:

LoopViewPager的setCurrentItem方法代碼

public void setCurrentItem(int item, boolean smoothScroll) {
        int realItem = mAdapter.toInnerPosition(item);
        super.setCurrentItem(realItem, smoothScroll);
    }

而LoopPagerAdapterWrapper 的toInnerPosition方法:

 public int toInnerPosition(int realPosition) {
        int position = (realPosition + 1);
        return position;
    }

沒錯,就是我前面提到的,因為左邊額外加了一個界面(就是上圖的<0>位置),所以我們的起始時候是從<1>位置開始。所以如果用戶在activity代碼里面執(zhí)行LoopViewPager.setCurrentItem(N, smoothScroll);實際上跳到的都是N+1的位置。

好了,接下來我們來看第一步中。LoopPagerAdapterWrapper把我們傳入的PageAdapter進(jìn)行封裝,到底做了什么處理。

我們知道繼承PagerAdapter,一般是要實現(xiàn)以下幾個方法

  • 構(gòu)造函數(shù)
  • getCount
  • instantiateItem
  • destroyItem
  • isViewFromObject

我們就這幾個主要方法一一來看。

構(gòu)造函數(shù):
//構(gòu)造函數(shù),既LoopPagerAdapterWrapper里面的mAdapter就是我們傳入的PagerAdapter
LoopPagerAdapterWrapper(PagerAdapter adapter) {
        this.mAdapter = adapter;
    }
getCount:

然后在getCount方法我們發(fā)現(xiàn)跟我們前面說的一樣,因為要增加頭尾二個界面,所以count這時候要在我們傳入的PagerAdapter的個數(shù)基礎(chǔ)上再加上2。

@Override
public int getCount() {
     return mAdapter.getCount() + 2;
}
instantiateItem:
    @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);
    }

我們發(fā)現(xiàn)最后調(diào)用的是我們自己的那個mAdapter的instantiateItem方法,而傳入的第二個參數(shù)realPosition被經(jīng)過處理,即:

 int realPosition = (mAdapter instanceof FragmentPagerAdapter || mAdapter instanceof FragmentStatePagerAdapter)
                ? position
                : toRealPosition(position);

因為我們當(dāng)前先展示的是View界面的循環(huán)切換,所以最后是
int realPosition = toRealPosition(position);

我們再看toRealPosition方法到底對我們的position參數(shù)做了什么處理:

int toRealPosition(int position) {
        int realCount = getRealCount();
        if (realCount == 0)
            return 0;
        int realPosition = (position-1) % realCount;
        if (realPosition < 0)
            realPosition += realCount;

        return realPosition;
    }

public int getRealCount() {    
       return mAdapter.getCount();
}

所以我產(chǎn)生以下理解:

所以就是說我們在比如顯示LoopPagerAdapterWrapper的第一個界面的時候。其實是調(diào)用我們自己寫的PagerAdapter來創(chuàng)建界面,然后創(chuàng)建的是自己寫的PagerAdapter的最后一個界面。這樣肯定需要一個公式來對應(yīng)。

就拿現(xiàn)在這個四個界面來寫說。創(chuàng)建第一個界面時候。是在LoopPagerAdapterWrapper里面position是0,因為是為了實現(xiàn)循環(huán),所以理論上是要顯示TWO這個界面。但是因為最后是用自己寫的PagerAdapter來進(jìn)行創(chuàng)建,也就是我們的adapter中的position為1,才是TWO這個界面,

我們知道我們其實只想要二個界面,也就是ONE和TWO(即你自己寫的Adapter中的<0>和<1>二個界面),但為了實現(xiàn)循環(huán),其實偷偷的給我們制造了四個界面(即《0》,《1》,《2》,《3》四個界面)。
我用《》和<>分別代表二個Adapter中的界面的position。
所以對應(yīng)的關(guān)系是上面那個toRealPosition的算法。

具體來看就是:

實際四個界面: 《0》 《1》 《2》 《3》
想要的二個界面: <1> <0> <1> <0>
擴展:

如果我們想要的是四個界面,我們自己寫的PagerAdapter中分別顯示文字ONE,TWO,THREE,F(xiàn)OUR。就是position為0-3。為了循環(huán),我們的PagerAdapter會用LoopPagerAdapterWrapper來封裝,會增加二個位置,LoopPagerAdapterWrapper的position就變成了0-5。

實際六個界面: 《0》 《1》 《2》 《3》 《4》 《5》
想要的四個界面: <3> <0> <1> <2> <3> <0>

所以這就好理解了。比如在LoopPagerAdapterWrapper的instantiateItem方法里面的position要轉(zhuǎn)換過后,再傳給自己寫的PagerAdapter的instantiateItem方法里面。

通過上面的提到過的toRealPosition方法,我們發(fā)現(xiàn)就可以把數(shù)字進(jìn)行轉(zhuǎn)換。
0-->3 , 1-->0 , 2-->1 , 3-->2, 4-->3 , 5-->0。

destroyItem:
@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);
        }
    }

這時候看起來是不是和上面的instantiateItem方法差不多。哈哈。估計大家這時候應(yīng)該都看得懂了。我也不多做分析了。??

isViewFromObject:
@Override
public boolean isViewFromObject(View view, Object object) {    
         return mAdapter.isViewFromObject(view, object);
}

就是調(diào)用自己寫的PagerAdapter的isViewFromObject方法。

好的,這樣大概就知道了LoopPagerAdapterWrapper對我們的自定義的PagerAdapter做了哪些封裝處理。那當(dāng)我們滑到最后一個,再滑動就會自動回到第一個是如何實現(xiàn)的?我們繼續(xù)分析下去

如何循環(huán)從最后回到開始

我們前面提過。比如

四個界面

從位置2的的TWO的界面再向左邊移動的時候,滑動過程顯示位置3的ONE,然后滑動結(jié)束后。實際上是通過ViewPager的setCurrentItem方法跳轉(zhuǎn)到了位置1的ONE。

因為LoopViewPager是繼承ViewPager。我們來看LoopViewPager的源碼做了什么處理:

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);
            }
        }
    };

}

這個接口不知道的可以再看一遍以下這篇文章。
Android開發(fā):ViewPage滑動接口最詳細(xì)解析

根據(jù)上面代碼我們可以看在,在LoopViewPager中自定義了OnPageChangeListener接口,然后賦值給了LoopViewPager。所以在LoopViewPager在滑動的時候會調(diào)用它的onPageSelected,onPageScrolled,onPageScrollStateChanged方法。

在onPageScrolled方法里面

if (mAdapter != null) {
        realPosition = mAdapter.toRealPosition(position);
        if (positionOffset == 0
                && mPreviousOffset == 0
                && (position == 0 || position == mAdapter.getCount() - 1)) {
            setCurrentItem(realPosition, false);
        }
}

和onPageScrollStateChanged里面的

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);
        }
}

這下就知道了吧。這下就知道了為啥最后又能回到前面的界面去了。哈哈

-----------------------------------先結(jié)尾分割線割一下----------------------------------

文章發(fā)現(xiàn)好長啊。View+ViewPager討論先到這里。后面再補上Fragment+ViewPager的討論

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容