ViewPager是v4支持庫中的一個(gè)控件,相信幾乎所有接觸Android開發(fā)的人都對(duì)它不陌生。之所以還要在這里翻舊賬,是因?yàn)槲以谧罱捻?xiàng)目中有多個(gè)需求用到了它,覺得自己對(duì)它的認(rèn)識(shí)不夠深刻。我計(jì)劃從最簡(jiǎn)單的使用場(chǎng)景出發(fā),記錄我到目前為止所對(duì)ViewPager的使用情況以及有關(guān)它的一些知識(shí)點(diǎn)。
這個(gè)系列的代碼將存放在Github倉庫中,每篇文章對(duì)應(yīng)一個(gè)分支或幾個(gè)分支。
這是第三篇文章,將討論集中有關(guān)如何使用ViewPager展示無限循環(huán)視圖的方法。
方法1:極大化PagerAdapter.getCount的返回值
這是最簡(jiǎn)單的實(shí)現(xiàn)方法。關(guān)鍵在于重寫PagerAdapter.getCount方法,將其返回值設(shè)置為Integer.MAX_VALUE,然后通通過取模position%count的方式獲取得對(duì)應(yīng)的數(shù)據(jù)進(jìn)行視圖渲染。
...
@Override
public int getCount() {
return Integer.MAX_VALUE;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
int index = position % 3;
String text = texts.get(index);
TextView textView = new TextView(container.getContext());
textView.setText(text);
container.addView(textView);
return textView;
}
...
這種方法畢竟不是真實(shí)的無限循環(huán),只是虛擬了一個(gè)極大的頁數(shù),讓用戶翻頁的時(shí)候很觸及到“世界的盡頭”。所以在初始化的時(shí)候需要完成一個(gè)關(guān)鍵初始化:
viewPager.setCurrentItem(Integer.MAX_VALUE / 2, false);
把初始化頁面定位到世界的中央。
相關(guān)代碼在分支:03-fake-infinite-cycle可以獲取。
方法2:在數(shù)據(jù)源首尾添加重復(fù)節(jié)點(diǎn)
這是實(shí)現(xiàn)ViewPager無限循環(huán)的另一種方案:通過在數(shù)據(jù)源的首尾處添加重復(fù)的數(shù)據(jù)(在源數(shù)據(jù)前插入最后一個(gè)數(shù)據(jù),其后插入原來的第一個(gè)數(shù)據(jù)),這兩個(gè)重復(fù)數(shù)據(jù)的作用是在滾動(dòng)過程中作為中間視圖,當(dāng)滾動(dòng)停止時(shí)立刻切換到最終的視圖,進(jìn)入下一個(gè)滾動(dòng)循環(huán)。
相關(guān)代碼見分支:03-infinite-cycle-with-additional-views
首先在往PagerAdapter插入數(shù)據(jù)的時(shí)候?qū)?shù)據(jù)進(jìn)行一下處理:
public void setTexts(List<String> texts) {
this.texts.clear();
if (texts == null) {
notifyDataSetChanged();
return;
}
// 只有一個(gè)數(shù)據(jù)時(shí)不循環(huán)
if (texts.size() == 1) {
this.texts.addAll(texts);
// 多個(gè)數(shù)據(jù),插入重復(fù)數(shù)據(jù)
} else if (texts.size() > 1) {
this.texts.add(texts.get(texts.size() - 1));
this.texts.addAll(texts);
this.texts.add(texts.get(0));
}
notifyDataSetChanged();
}
其次讓ViewPager實(shí)現(xiàn)ViewPager.OnPageChangeListener接口,監(jiān)聽滾動(dòng)狀態(tài)。代碼如下:
@Override
public void onPageSelected(int position) {
int realCount = getCount() - 2;
// 多于1,才會(huì)循環(huán)跳轉(zhuǎn)
if ( getCount() > 1) {
// 首位之前,跳轉(zhuǎn)到末尾(N)
if ( position < 1) {
position = realCount;
viewPager.setCurrentItem(position,false);
}
// 末位之后,跳轉(zhuǎn)到首位(1)
else if ( position > realCount) {
position = 1;
viewPager.setCurrentItem(position,false);
}
}
}
最后組裝一下ViewPager和PagerAdapter即可:
viewPager.setAdapter(adapter);
viewPager.addOnPageChangeListener(adapter);
if (adapter.getCount() > 1) {
viewPager.setCurrentItem(1, false);
}
注意最后的if語句,它讓ViewPager默認(rèn)顯示第一頁。否則頁面將展示最后一個(gè)源數(shù)據(jù)的內(nèi)容且無法向右滑動(dòng)。
實(shí)際上這種方法也是有缺陷的。當(dāng)用戶滑動(dòng)ViewPager到源數(shù)據(jù)的最后一個(gè)節(jié)點(diǎn)(下標(biāo):getCount()-2)并且先要繼續(xù)滑動(dòng)顯示下一個(gè)節(jié)點(diǎn)時(shí),這期間ViewPager首先隨用戶手指一動(dòng)正常展示我們插入的重復(fù)內(nèi)容(下標(biāo):getCount()-1),當(dāng)滾動(dòng)停止且觸發(fā)了onPageSelected回調(diào),ViewPager立即切換到源數(shù)據(jù)的第一頁(下標(biāo):1)進(jìn)入下一個(gè)循環(huán)。這會(huì)導(dǎo)致幾個(gè)不協(xié)調(diào)的現(xiàn)象:
- 切換到下一個(gè)循環(huán)的時(shí)候會(huì)破壞
ViewPager的滾動(dòng)動(dòng)畫(如:滾動(dòng)慣性動(dòng)畫)。 - 切換前展示的緩存視圖在切換時(shí)被銷毀,切換后的視圖需要重新生成。如果這里有需要延遲加載的內(nèi)容也會(huì)導(dǎo)致展示不協(xié)調(diào)。
方法3:改進(jìn)方法2
針對(duì)上述方法2提出的兩個(gè)缺點(diǎn),在此將著重解決缺點(diǎn)1出現(xiàn)的動(dòng)畫不連貫的現(xiàn)象,作為第三種方案進(jìn)行介紹。至于缺點(diǎn)2可以通過緩存視圖的方式解決,就不在此贅述。
方法3的代碼見分支:03-infinite-cycle-better-practise
該方案已經(jīng)滿足我目前的需求。它的關(guān)鍵點(diǎn)如下:
首先,如方法2一樣在數(shù)據(jù)源頭尾插入重復(fù)節(jié)點(diǎn),用于過渡。這里我重新寫了setTexts方法,讓只有一個(gè)數(shù)據(jù)的場(chǎng)景也可以循環(huán):
public void setTexts(List<String> texts) {
this.count = 0;
this.texts.clear();
if (texts != null && texts.size() > 0) {
this.count = texts.size();
for (int i = 0; i <= count + 1; i++) {
if (i == 0) {
this.texts.add(texts.get(count - 1));
} else if (i == count + 1) {
this.texts.add(texts.get(0));
} else {
this.texts.add(texts.get(i - 1));
}
}
}
notifyDataSetChanged();
}
接下來解決方法2的動(dòng)畫不連貫的問題。注意到在方法2中在OnPageChangeListener的onPageSelected方法中處理了循環(huán)的跳轉(zhuǎn)邏輯。然后onPageSelected是ViewPager處理ACTION_UP事件時(shí)回調(diào)的。也就是說,當(dāng)用戶的手指時(shí)快速拖動(dòng)后離開ViewPager時(shí),ViewPager回調(diào)了該方法,然后還會(huì)繼續(xù)后續(xù)的衰減動(dòng)畫。在這個(gè)時(shí)間點(diǎn)使用setCurrentItem跳轉(zhuǎn)到指定視圖必然會(huì)造成動(dòng)畫停頓的問題。
把切換循環(huán)改在ViewPager的滾動(dòng)狀態(tài)發(fā)生變化時(shí)進(jìn)行。怎么做呢?見代碼:
// count為源數(shù)據(jù)的條目
// currentItem為PagerAdapter當(dāng)前選中項(xiàng)
@Override
public void onPageSelected(int position) {
currentItem = position;
}
@Override
public void onPageScrollStateChanged(int state) {
switch (state) {
case ViewPager.SCROLL_STATE_IDLE://No operation
if (currentItem == 0) {
viewPager.setCurrentItem(count, false);
} else if (currentItem == count + 1) {
viewPager.setCurrentItem(1, false);
}
break;
case ViewPager.SCROLL_STATE_DRAGGING: //start Sliding
if (currentItem == 0) {
viewPager.setCurrentItem(count, false);
} else if (currentItem == count + 1) {
viewPager.setCurrentItem(1, false);
}
break;
case ViewPager.SCROLL_STATE_SETTLING://end Sliding
break;
}
}
代碼中在狀態(tài)變?yōu)橥V埂?code>SCROLL_STATE_IDLE”或狀態(tài)變?yōu)殚_始滾動(dòng)“SCROLL_STATE_DRAGGING”時(shí)處理了循環(huán)切換的邏輯。
這里描述一下整個(gè)流程。如果用戶處于第一頁且繼續(xù)向右滑動(dòng)手指,或者處于最后一頁且繼續(xù)向左滑動(dòng)手指時(shí),在狀態(tài)由空閑變?yōu)殚_始滾動(dòng)“SCROLL_STATE_DRAGGING”進(jìn)行切換。第一種情況,如果最終成功切換到目標(biāo)頁面,那么在狀態(tài)變?yōu)榭臻e時(shí)由于currentItem已經(jīng)發(fā)生變化,所以不會(huì)重復(fù)切換。第二種情況,如果沒有成功切換到目標(biāo)頁面,ViewPager需要在狀態(tài)變?yōu)椤?code>SCROLL_STATE_IDLE”時(shí)再次切換回原來的視圖。
注意在初始化ViewPager時(shí)調(diào)用一下setCurrentItem(1),讓它正確顯示第一個(gè)視圖。
小結(jié)
ViewPager循環(huán)展示數(shù)據(jù)的方法目前就介紹到這里。我認(rèn)為方法1和方法3可以根據(jù)不同場(chǎng)景考慮是否使用。出于某種情結(jié),我更傾向于使用方法3,畢竟方法三是查看了github中的banner庫之后總結(jié)出來的。
同步博客