
前提:前幾天無聊,下了個《讀者》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
看了下面那篇文章就應(yīng)該知道如何初步使用ViewPager
Android開發(fā):ViewPage詳細(xì)使用教程。看了下面這篇你就會知道當(dāng)ViewPager在滑動的時候調(diào)用的相關(guān)接口。
Android開發(fā):ViewPage滑動接口最詳細(xì)解析
-------------------------------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)

在研究前我們要先學(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)簽。

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


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


然后可以看到效果:


然后手指繼續(xù)往左滑動,會送Fragment_TWO 又回到了Fragment_ONE的界面。
SO ------ WHY ?
我先來講解一下大概思路。這樣大家后面看講解的時候就會更容易理解,
比如現(xiàn)在有二個View要循環(huán)切換,顯示的是ONE 和 TWO

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

既然我們最左邊加了一個<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的討論