ViewPager使用記錄3——循環(huán)展示

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

最后組裝一下ViewPagerPagerAdapter即可:

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)象:

  1. 切換到下一個(gè)循環(huán)的時(shí)候會(huì)破壞ViewPager的滾動(dòng)動(dòng)畫(如:滾動(dòng)慣性動(dòng)畫)。
  2. 切換前展示的緩存視圖在切換時(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中在OnPageChangeListeneronPageSelected方法中處理了循環(huán)的跳轉(zhuǎn)邏輯。然后onPageSelectedViewPager處理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é)出來的。

同步博客

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

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

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