ViewPager是v4支持庫中的一個控制項,相信幾乎所有接觸Android開發的人都對它不陌生。之所以還要在這裡翻舊賬,是因為我在最近的項目中有多個需求用到了它,覺得自己對它的認識不夠深刻。我計劃從最簡單的使用場景出發,記錄我到目前為止所對ViewPager的使用情況以及有關它的一些知識點。 這個系 ...
ViewPager是v4支持庫中的一個控制項,相信幾乎所有接觸Android開發的人都對它不陌生。之所以還要在這裡翻舊賬,是因為我在最近的項目中有多個需求用到了它,覺得自己對它的認識不夠深刻。我計劃從最簡單的使用場景出發,記錄我到目前為止所對ViewPager的使用情況以及有關它的一些知識點。
這個系列的代碼將存放在Github倉庫中,每篇文章對應一個分支。
這是第一篇文章,講述關於ViewPager展示動態數據的方法與相關知識點。ViewPager展示動態數據的方法有好幾種,彙總到一起都離不開PagerAdapter的一個方法getItemPosition。下麵討論一下具體方法。
錯誤的示例
上一篇文章介紹瞭如何展示靜態數據。代碼中的自定義的PagerAdapter中有一個setTexts方法,它的內容是這樣的:
public synchronized void setTexts(List<String> texts) { this.texts.clear(); if (texts != null && texts.size() > 0) { this.texts.addAll(texts); } notifyDataSetChanged(); }
它的作用就是清除適配器列表中原有的數據,然後把外部傳遞進來的數據複製進列表,最後通知適配器更新。
外部數據變更時,直接調用該方法:
adapter.setTexts(randomData());
看起來好像很合理,畢竟ListView等組件的適配器就是這麼用的。
但是如果你真的運行起來就會發現ViewPager的展示的數據並不是如你所願的更新了。數據變多時,前面的數據不更新;數據變少時,在頁面展示最後一項的情況下還可以向左滑動看到部分舊數據;甚至出現白屏的情況。
代碼見Github。
那怎麼正確更新?
使用ViewPager.setAdapter切換數據源
這是最簡單的修改數據的方法,適合在整個數據源都發生變化的場景下使用。
在初始化ViewPager的使用我們使用下麵的代碼:
adapter = new DynamicDataSetAdapter(); adapter.setTexts(randomData()); viewPager = (ViewPager) findViewById(R.id.vp_viewpager_update); viewPager.setAdapter(adapter);
當需要讓ViewPager展示的數據改變時,我們可以:
// 可以選擇創建新的PagerAdapter對象或使用已有的對象 // adapter = new DynamicDataSetAdapter(); adapter.setTexts(randomData()); viewPager.setAdapter(adapter);
代碼見Github。
為什麼使用notifyDatasetChanged無法正確更新數據,而setAdapter可以?這要求我們瞭解一下ViewPager的更新原理。
ViewPager的更新原理
看ViewPager的源碼會發現它擁有兩個成員變數,分別是:
PagerAdapter mAdapter; private PagerObserver mObserver;
PagerObserver是在ViewPager內部定義的私有類,也就是說它預設持有了ViewPager的引用,因此可以調用ViewPager的方法。
private class PagerObserver extends DataSetObserver { PagerObserver() { } @Override public void onChanged() { dataSetChanged(); } @Override public void onInvalidated() { dataSetChanged(); } }
其實看PagerObserver的名字就知道這是一個觀察者。PagerAdapter以PagerObserver為通道告知ViewPager調用dataSetChanged方法更新數據。
看dataSetChanged方法的源碼,關鍵在:
... for (int i = 0; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); final int newPos = mAdapter.getItemPosition(ii.object); if (newPos == PagerAdapter.POSITION_UNCHANGED) { continue; } ... } ...
我們看到dataSetChanged方法調用了PagerAdapter的getItemPosition方法了。一旦該方法返回了 PagerAdapter.POSITION_UNCHANGED就不刷新這個頁面了。
由於我們使用了預設的getItemPosition方法,而預設的getItemPosition方法的實現恰好就返回了這個值:
public int getItemPosition(Object object) { return POSITION_UNCHANGED; }
到這裡我們就明白了為什麼修改數據後只調用notifyDataSetChanged不會刷新頁面了。
接下來看一下setAdapter方法的源碼。關鍵在於該方法的前面幾行:
if (mAdapter != null) { mAdapter.setViewPagerObserver(null); mAdapter.startUpdate(this); for (int i = 0; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); mAdapter.destroyItem(this, ii.position, ii.object); } mAdapter.finishUpdate(this); mItems.clear(); removeNonDecorViews(); mCurItem = 0; scrollTo(0, 0); } ...
可以看到一旦就的數據適配器不為null,ViewPager就會銷毀所有原來與數據相關的ItemInfo,並且逐一調用適配器的destroyItem方法銷毀視圖。
setAdapter之後的代碼就不用看了,跟初始化流程一樣生成ItemInfo列表與相關數據。
這就是為什麼調用setAdapter可以更新數據的原因。
重寫getItemPosition實現更高效的數據更新
既然知道了getItemPosition決定了數據更新的規則,我們只要重寫這個方法就可以了。
最暴力的方法當然是直接讓這個方法返回POSITION_NONE,在效果上這跟使用setAdapter沒什麼區別了,只要調用了PagerAdapter的notifyDatasetChanged就會導致銷毀原有的數據並重建。
更穩妥一點的方法是配合應用的實際業務數據進行該方法的定製。下麵提供一個僅供參考的例子:
private static class DynamicDataSetAdapter extends PagerAdapter { private List<String> texts; public DynamicDataSetAdapter() { texts = new ArrayList<>(); } @Override public int getCount() { return texts.size(); } @Override public boolean isViewFromObject(View view, Object object) { return object.equals(view); } @Override public Object instantiateItem(ViewGroup container, int position) { String text = texts.get(position); TextView textView = new TextView(container.getContext()); textView.setTag(text); textView.setText(text); container.addView(textView); return textView; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } @Override public int getItemPosition(Object object) { View view = (View) object; String text = (String) view.getTag(); if (text == null) { return PagerAdapter.POSITION_NONE; } int index = this.texts.indexOf(text); if (index == -1) { return PagerAdapter.POSITION_NONE; } return index; } public synchronized void setTexts(List<String> texts) { this.texts.clear(); if (texts != null && texts.size() > 0) { this.texts.addAll(texts); } notifyDataSetChanged(); } }
代碼見Github。
本文來自作者同步博客