ViewPager是v4支持庫中的一個控制項,相信幾乎所有接觸Android開發的人都對它不陌生。之所以還要在這裡翻舊賬,是因為我在最近的項目中有多個需求用到了它,覺得自己對它的認識不夠深刻。我計劃從最簡單的使用場景出發,記錄我到目前為止所對ViewPager的使用情況以及有關它的一些知識點。 這個系 ...
ViewPager是v4支持庫中的一個控制項,相信幾乎所有接觸Android開發的人都對它不陌生。之所以還要在這裡翻舊賬,是因為我在最近的項目中有多個需求用到了它,覺得自己對它的認識不夠深刻。我計劃從最簡單的使用場景出發,記錄我到目前為止所對ViewPager的使用情況以及有關它的一些知識點。
這個系列的代碼將存放在Github倉庫中,每篇文章對應一個分支或幾個分支。
這是第三篇文章,將討論集中有關如何使用ViewPager展示無限迴圈視圖的方法。
方法1:極大化PagerAdapter.getCount
的返回值
這是最簡單的實現方法。關鍵在於重寫PagerAdapter.getCount
方法,將其返回值設置為Integer.MAX_VALUE
,然後通通過取模position%count
的方式獲取得對應的數據進行視圖渲染。
... @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; } ...
這種方法畢竟不是真實的無限迴圈,只是虛擬了一個極大的頁數,讓用戶翻頁的時候很觸及到“世界的盡頭”。所以在初始化的時候需要完成一個關鍵初始化:
viewPager.setCurrentItem(Integer.MAX_VALUE / 2, false);
把初始化頁面定位到世界的中央。
相關代碼在分支:03-fake-infinite-cycle可以獲取。
方法2:在數據源首尾添加重覆節點
這是實現ViewPager
無限迴圈的另一種方案:通過在數據源的首尾處添加重覆的數據(在源數據前插入最後一個數據,其後插入原來的第一個數據),這兩個重覆數據的作用是在滾動過程中作為中間視圖,當滾動停止時立刻切換到最終的視圖,進入下一個滾動迴圈。
相關代碼見分支:03-infinite-cycle-with-additional-views
首先在往PagerAdapter插入數據的時候對數據進行一下處理:
public void setTexts(List<String> texts) { this.texts.clear(); if (texts == null) { notifyDataSetChanged(); return; } // 只有一個數據時不迴圈 if (texts.size() == 1) { this.texts.addAll(texts); // 多個數據,插入重覆數據 } 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實現ViewPager.OnPageChangeListener
介面,監聽滾動狀態。代碼如下:
@Override public void onPageSelected(int position) { int realCount = getCount() - 2; // 多於1,才會迴圈跳轉 if ( getCount() > 1) { // 首位之前,跳轉到末尾(N) if ( position < 1) { position = realCount; viewPager.setCurrentItem(position,false); } // 末位之後,跳轉到首位(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預設顯示第一頁。否則頁面將展示最後一個源數據的內容且無法向右滑動。
實際上這種方法也是有缺陷的。當用戶滑動ViewPager
到源數據的最後一個節點(下標:getCount()-2)並且先要繼續滑動顯示下一個節點時,這期間ViewPager
首先隨用戶手指一動正常展示我們插入的重覆內容(下標:getCount()-1),當滾動停止且觸發了onPageSelected
回調,ViewPager立即切換到源數據的第一頁(下標:1)進入下一個迴圈。這會導致幾個不協調的現象:
- 切換到下一個迴圈的時候會破壞
ViewPager
的滾動動畫(如:滾動慣性動畫)。 - 切換前展示的緩存視圖在切換時被銷毀,切換後的視圖需要重新生成。如果這裡有需要延遲載入的內容也會導致展示不協調。
方法3:改進方法2
針對上述方法2提出的兩個缺點,在此將著重解決缺點1出現的動畫不連貫的現象,作為第三種方案進行介紹。至於缺點2可以通過緩存視圖的方式解決,就不在此贅述。
方法3的代碼見分支:03-infinite-cycle-better-practise
該方案已經滿足我目前的需求。它的關鍵點如下:
首先,如方法2一樣在數據源頭尾插入重覆節點,用於過渡。這裡我重新寫了setTexts
方法,讓只有一個數據的場景也可以迴圈:
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的動畫不連貫的問題。註意到在方法2中在OnPageChangeListener
的onPageSelected
方法中處理了迴圈的跳轉邏輯。然後onPageSelected
是ViewPager
處理ACTION_UP
事件時回調的。也就是說,當用戶的手指時快速拖動後離開ViewPager
時,ViewPager
回調了該方法,然後還會繼續後續的衰減動畫。在這個時間點使用setCurrentItem
跳轉到指定視圖必然會造成動畫停頓的問題。
把切換迴圈改在ViewPager
的滾動狀態發生變化時進行。怎麼做呢?見代碼:
// count為源數據的條目 // currentItem為PagerAdapter當前選中項 @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; } }
代碼中在狀態變為停止“SCROLL_STATE_IDLE
”或狀態變為開始滾動“SCROLL_STATE_DRAGGING
”時處理了迴圈切換的邏輯。
這裡描述一下整個流程。如果用戶處於第一頁且繼續向右滑動手指,或者處於最後一頁且繼續向左滑動手指時,在狀態由空閑變為開始滾動“SCROLL_STATE_DRAGGING
”進行切換。第一種情況,如果最終成功切換到目標頁面,那麼在狀態變為空閑時由於currentItem
已經發生變化,所以不會重覆切換。第二種情況,如果沒有成功切換到目標頁面,ViewPager
需要在狀態變為“SCROLL_STATE_IDLE
”時再次切換回原來的視圖。
註意在初始化ViewPager
時調用一下setCurrentItem(1)
,讓它正確顯示第一個視圖。
小結
ViewPager
迴圈展示數據的方法目前就介紹到這裡。我認為方法1和方法3根據不同場景考慮是否使用。出於某種情結,我更傾向於使用方法3,畢竟方法三是查看了github中的banner庫之後總結出來的。
本文來自作者同步博客