一、PagerAdapter介紹
PagerAdapter簡介
ListView 大家應該都很熟悉吧!ListView 一般都需要一個 Adapter 來填充數據,如 ArrayAdapter、SimpleAdapter。PagerAdapter 就是 ViewPager 的 Adapter,與 ListView 的 Adapter 作用一樣。
ViewPager->PageAdapter == ListView->BaseAdapter
先看下官方介紹
官方介紹

PageAdapter 繼承自 Object,繼承結構參考意義不大,那老實看文檔。文檔上沒有提供示例代碼,只是說了下要自定義 PageAdapter 需要實現下面四個方法:
- instantiateItem(ViewGroup container, int position):該方法的功能是創建指定位置的頁面視圖。適配器有責任增加即將創建的 View 視圖到這里給定的 container 中,這是為了確保在 finishUpdate(viewGroup) 返回時 this is be done!
返回值:返回一個代表新增視圖頁面的 Object(Key),這里沒必要非要返回視圖本身,也可以這個頁面的其它容器。其實我的理解是可以代表當前頁面的任意值,只要你可以與你增加的 View 一一對應即可,比如 position 變量也可以做為 Key - destroyItem(ViewGroup container, int position, Object object):該方法的功能是移除一個給定位置的頁面。適配器有責任從容器中刪除這個視圖,這是為了確保在 finishUpdate(viewGroup) 返回時視圖能夠被移除
- getCount():返回當前有效視圖的數量
- isViewFromObject(View view, Object object):該函數用來判斷 instantiateItem() 函數所返回來的 Key 與一個頁面視圖是否是代表的同一個視圖(即它倆是否是對應的,對應的表示同一個 View)
返回值:如果對應的是同一個View,返回 true,否則返回 false
上面對 PageAdapter 的四個抽象方法做了簡要說明,下面看看如何使用
簡單使用
mContentVP.setAdapter(new PagerAdapter() { @Override public int getCount() { return dataList.size(); } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public Object instantiateItem(ViewGroup container, int position) { View view = View.inflate(SimpleDemoActivity.this, R.layout.item_vp_demopageradapter, null); TextView pageNumTV = (TextView) view.findViewById(R.id.tv_pagenum); pageNumTV.setText("" + dataList.get(position)); container.addView(view); return view; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } });
可以看到實現 PagerAdapter 與 BaseAdapter 很類似,只是 PagerAdapter 的 isViewFromObject() 與 instantiateItem() 方法需要好好理解下。
PagerAdapter 刷新的問題
提出問題
在使用 ListView 的時候,我們往往習慣了更新 Adapter 的數據源,然后調用 Adapter 的 notifyDataSetChanged() 方法來刷新列表(有沒有點 MVC 的感覺)。
PagerAdapter 也有 notifyDataSetChanged() 方法,那我們按照這個流程來試試,看有沒有什么問題。(ListView 的示例就不在這里演示了,感興趣的可以自己去試試,非常簡單)
那么我的問題是:“ViewPager 的 PagerAdapter 在數據源更新后,能否自動刷新視圖?”
帶着問題,我們做一些實驗,下面實驗的思路是:修改數據源,然后通知 PagerAdapter 更新,查看視圖的變化。
實驗環境准備
看看實驗環境,上代碼:
private void initData() { // 數據源 mDataList = new ArrayList<>(5); mDataList.add("Java"); mDataList.add("Android"); mDataList.add("C&C++"); mDataList.add("OC"); mDataList.add("Swift"); // 很簡單的一個 PagerAdapter this.mContentVP.setAdapter(mPagerAdapter = new PagerAdapter() { @Override public int getCount() { return mDataList.size(); } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public Object instantiateItem(ViewGroup container, int position) { View view = View.inflate(SimpleDemoActivity.this, R.layout.item_vp_demopageradapter, null); TextView pageNumTV = (TextView) view.findViewById(R.id.tv_pagenum); pageNumTV.setText("DIY-PageNum-" + mDataList.get(position)); container.addView(view); return view; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } }); }
PagerAdapter 刷新實驗
1、更新數據源中的某項
對應代碼:
private void refresh() { mDataList.set(0, "更新數據源測試"); mPagerAdapter.notifyDataSetChanged(); }
問題描述:在演示動畫中可以看到,更新數據源之后視圖並沒有立即刷新,多滑動幾次再次回到更新的 Item 時才更新(這里先看問題,下面會細說)。
2、往數據源中添加數據
對應代碼:
private void add() { mDataList.add("這是新添加的Item"); mPagerAdapter.notifyDataSetChanged(); }
問題描述:沒什么問題,數據源添加數據后通知 PagerAdapter 刷新,ViewPager 中就多了一個 Item。
3、從數據源中刪除數據
private void delete() { mDataList.remove(0); mPagerAdapter.notifyDataSetChanged(); }
問題描述:這個問題就較多了,首先,如果是刪除當前 Item,那么會看到沒有任何反應;其次,如果刪除的不是當前 Item,會發現出現了數據錯亂,並且后面有 Item 滑不過去,但是按住往后滑的時候可以看到后面的 Item。
4、將數據源清空
private void clean() { mDataList.clear(); mPagerAdapter.notifyDataSetChanged(); }
問題描述:從上面的動圖可以看到,清空數據源之后,會殘留一個 Item。
說明:先不要計較上面所寫的 PagerAdapter 是否有問題,這里只是想引出問題來,下面會針對 PagerAdapter、FragmentPagerAdapter 以及 FragmentStatePagerAdapter 來分析問題原因和給出解決方案。
二、PagerAdapter
從上面的實驗可以看出 ViewPager 不同於 ListView,如果單純的調用 ViewPager.getAdapter().notifyDataSetChanged() 方法(即 PagerAdapter 的 notifyDataSetChanged()方法)頁面並沒有刷新。
PagerAdapter 用於 ViewPager 的 Item 為普通 View的情況,這個相對簡單,所以最先介紹。
相信很多同學都搜過類似的問題 —— “PagerAdapter 的 notifyDataSetChanged() 不刷新?”。有的說這是 bug,有的則認為 Google 是特意這樣設計的,個人傾向后一種觀點(我覺得這是 Google 為了 ViewPager 性能考慮而設計的,畢竟 ViewPager 需要顯示“很多大的”視圖,而且要防止用戶滑動時覺得卡頓)。
ViewPager 刷新分析
先來了解下 ViewPager 的刷新過程:
1、刷新的起始
ViewPager 的刷新是從調用其 PagerAdapter 的 notifyDataSetChanged() 方法開始的,那先看看該方法的源碼(在源碼面前一切無所遁形...):
/** * This method should be called by the application if the data backing this adapter has changed * and associated views should update. */ public void notifyDataSetChanged() { synchronized (this) { if (mViewPagerObserver != null) { mViewPagerObserver.onChanged(); } } mObservable.notifyChanged(); }
2、DataSetObservable 的 notifyChanged()
上面的方法中出現了兩個關鍵的成員變量:
private final DataSetObservable mObservable = new DataSetObservable(); private DataSetObserver mViewPagerObserver;
觀察者模式,有沒有?先不着急分析這個是不是觀察者模式,來看看 mObservable.notifyChanged() 做了些什么工作:
/** * Invokes {@link DataSetObserver#onChanged} on each observer. * Called when the contents of the data set have changed. The recipient * will obtain the new contents the next time it queries the data set. */ public void notifyChanged() { synchronized(mObservers) { // since onChanged() is implemented by the app, it could do anything, including // removing itself from {@link mObservers} - and that could cause problems if // an iterator is used on the ArrayList {@link mObservers}. // to avoid such problems, just march thru the list in the reverse order. for (int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onChanged(); } } }
notifyChanged() 方法中是很典型的觀察者模式中遍歷所有的 Observer,通知 變化發生了的代碼。代碼很簡單,那關鍵是這個 mObservers 包含哪些 Observer 呢?
3、DataSetObserver
直接從 mObservers 點進去你會發現這個:
protected final ArrayList<T> mObservers = new ArrayList<T>();
-_-',這是個泛型,坑了!還好 DataSetObservable 的 notifyChanged() 的注釋中寫了這些 Observer 是 DataSetObserver。那去看看 DataSetObserver:
public abstract class DataSetObserver { /** * This method is called when the entire data set has changed, * most likely through a call to {@link Cursor#requery()} on a {@link Cursor}. */ public void onChanged() { // Do nothing } /** * This method is called when the entire data becomes invalid, * most likely through a call to {@link Cursor#deactivate()} or {@link Cursor#close()} on a * {@link Cursor}. */ public void onInvalidated() { // Do nothing } }
一個抽象類,里面兩個空方法,這個好辦,找他的子類(AndroidStudio 中 將光標放到類名上,按 F4):

總算找到你了,就是用紅線框出來的那條,雙擊,定位過去。
4、PagerObserver 內部類
PagerObserver 是 ViewPager 中的一個內部類,實現也很簡單,就是調用了 ViewPager 中的 dataSetChanged() 方法,真正的關鍵來了。
private class PagerObserver extends DataSetObserver { @Override public void onChanged() { dataSetChanged(); } @Override public void onInvalidated() { dataSetChanged(); } }
5、ViewPager 的 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; } if (newPos == PagerAdapter.POSITION_NONE) { ... continue; } ... } ...
上面截取的代碼中 for 循環里面有兩個 continue 語句,這可能是比較關鍵的代碼,幸好不用我們繼續深入了,官方給出了解釋:
Called when the host view is attempting to determine if an item's position has changed. Returns POSITION_UNCHANGED if the position of the given item has not changed or POSITION_NONE if the item is no longer present in the adapter.The default implementation assumes that items will never change position and always returns POSITION_UNCHANGED.
大致的意思是:
如果 Item 的位置如果沒有發生變化,則返回 POSITION_UNCHANGED。如果返回了 POSITION_NONE,表示該位置的 Item 已經不存在了。默認的實現是假設 Item 的位置永遠不會發生變化,而返回 POSITION_UNCHANGED。
上面在源碼里面跟了一大圈是不是還是感覺沒有明朗,因為還有一個很關鍵的類 —— PagerAdapter 沒有介紹,再給點耐心,繼續。
6、PagerAdapter 的工作流程
其實就是 PagerAdapter 中方法的執行順序:
PagerAdapter 作為 ViewPager 的適配器,無論 ViewPager 有多少頁,PagerAdapter 在初始化時也只初始化開始的2個 View,即調用2次instantiateItem 方法。而接下來每當 ViewPager 滑動時,PagerAdapter 都會調用 destroyItem 方法將距離該頁2個步幅以上的那個 View 銷毀,以此保證 PagerAdapter 最多只管轄3個 View,且當前 View 是3個中的中間一個,如果當前 View 缺少兩邊的 View,那么就 instantiateItem,如里有超過2個步幅的就 destroyItem。
簡易圖示:
*
------+---+---+---+------
... 0 | 1 | 2 | 3 | 4 ... ------+---+---+---+------
當前 View 為2號 View,所以 PagerAdapter 管轄1、2、3三個 View,接下來向左滑動-->
*
------+---+---+---+------
... 1 | 2 | 3 | 4 | 5 ... ------+---+---+---+------
滑動后,當前 View 變為3號 View,PagerAdapter 會 destroyItem 0號View,instantiateItem 5號 View,所以 PagerAdapter 管轄2、3、4三個 View。
總結一下: Viewpager 的刷新過程是這樣的,在每次調用 PagerAdapter 的 notifyDataSetChanged() 方法時,都會激活 getItemPosition(Object object) 方法,該方法會遍歷 ViewPager 的所有 Item(由緩存的 Item 數量決定,默認為當前頁和其左右加起來共3頁,這個可以自行設定,但是至少會緩存2頁),為每個 Item 返回一個狀態值(POSITION_NONE/POSITION_UNCHANGED),如果是 POSITION_NONE,那么該 Item 會被 destroyItem(ViewGroup container, int position, Object object) 方法 remove 掉,然后重新加載,如果是 POSITION_UNCHANGED,就不會重新加載,默認是 POSITION_UNCHANGED,所以如果不重寫 getItemPosition(Object object),修改返回值,就無法看到 notifyDataSetChanged() 的刷新效果。
臨時解決方法
那就是直接一刀切:重寫 PagerAdapter 的 getItemPosition(Object object) 方法,將返回值固定為 POSITION_NON
上代碼:
@Override public int getItemPosition(Object object) { // 最簡單解決 notifyDataSetChanged() 頁面不刷新問題的方法 return POSITION_NONE; }
該方案的缺點:有個很明顯的缺陷,那就是會刷新所有的 Item,這將導致系統資源的浪費,所以這種方式不適合數據量較大的場景。
注意:
這種方式還有一個需要注意的地方,就是重寫 destoryItem() 方法:
@Override public void destroyItem(ViewGroup container, int position, Object object) { // 把 Object 強轉為 View,然后將 view 從 ViewGroup 中清除 container.removeView((View) object); }
方案的優化
這里提供一個思路,畢竟場景太多,相信大家理解了思路要實現就很簡單了,閑話不多說。
思路:在 instantiateItem() 方法中給每個 View 添加 tag(使用 setTag() 方法),然后在 getItemPosition() 方法中通過 View.getTag() 來判斷是否是需要刷新的頁面,是就返回 POSITION_NONE,否就返回 POSITION_UNCHANGED。
注意:這里有一點要注意的是,當清空數據源的時候需要返回 POSITION_NONE,可用如下代碼:
if (mDataList != null && mDataList.size()==0) { return POSITION_NONE; }
關於 PagerAdapter 的介紹就到這里了,雖然 FragmentPagerAdapter 與 FragmentStatePagerAdapter 都是繼承自 PagerAdapter。但是,這兩個是專門為以 Fragment 為 Item 的 ViewPager 所准備的,所以有其特殊性。且看下面的介紹。
三、FragmentPagerAdapter
簡介
上面通過使 getItemPosition() 方法返回 POSITION_NONE 到達數據源變化(也就是調用 notifyDataSetChanged())時,刷新視圖的目的。但是當我們使用 Fragment 作為 ViewPager 的 Item 時,就需要多考慮一些了,而且一般是使用 FragmentPagerAdapter 或者 FragmentStatePagerAdapter。
這里不展開討論 FragmentPagerAdapter 與 FragmentStatePagerAdapter 的異同和使用場景了,感興趣的可以看看這篇文章:FragmentPagerAdapter與FragmentStatePagerAdapter區別。
下面先來看看使用 FragmentPagerAdapter 時,如何在數據源發生變化時,刷新 Fragment 或者動態改變 Items 的數量。
方案:清除 FragmentManager 中緩存的 Fragment
實現上圖效果的關鍵代碼:
private void refresh() { if (checkData()) return; mDataList.set(0, 7); // 修改數據源 mPagerAdapter.updateData(mDataList); // 通知 Adapter 更新 } private void add() { mDataList.add(7); mPagerAdapter.updateData(mDataList); } private void delete() { if (checkData()) return; mDataList.remove(0); mPagerAdapter.updateData(mDataList); } private void clear() { if (checkData()) return; mDataList.clear(); mPagerAdapter.updateData(mDataList); }
2、FPagerAdapter1.java
public class FPagerAdapter1 extends FragmentPagerAdapter { private ArrayList<Fragment> mFragmentList; private FragmentManager mFragmentManager; public FPagerAdapter1(FragmentManager fm, List<Integer> types) { super(fm); this.mFragmentManager = fm; mFragmentList = new ArrayList<>(); for (int i = 0, size = types.size(); i < size; i++) { mFragmentList.add(FragmentTest.instance(i)); } setFragments(mFragmentList); } public void updateData(List<Integer> dataList) { ArrayList<Fragment> fragments = new ArrayList<>(); for (int i = 0, size = dataList.size(); i < size; i++) { Log.e("FPagerAdapter1", dataList.get(i).toString()); fragments.add(FragmentTest.instance(dataList.get(i))); } setFragments(fragments); } private void setFragments(ArrayList<Fragment> mFragmentList) { if(this.mFragmentList != null){ FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); for(Fragment f:this.mFragmentList){ fragmentTransaction.remove(f); } fragmentTransaction.commit(); mFragmentManager.executePendingTransactions(); } this.mFragmentList = mFragmentList; notifyDataSetChanged(); } @Override public int getCount() { return this.mFragmentList.size(); } public int getItemPosition(Object object) { return POSITION_NONE; } @Override public Fragment getItem(int position) { return mFragmentList.get(position); } }
3、思路分析
上面的代碼思路很簡單,就是當數據源發生變化時,先將 FragmentManger 里面所有緩存的 Fragment 全部清除,然后重新創建,這樣達到刷新視圖的目的。
但是,這樣做有一個缺點,那就是會造成不必要的浪費,會影響性能。還有就是必須使用一個 List 緩存所有的 Fragment,這也得占用不少內存...
思路挺簡單,這里不再贅述,那看看有沒有什么可以優化的。
優化:通過 Tag 獲取緩存的 Fragment
從上面的動圖上可以看到,更新某一個 Fragment 沒有問題,清空數據源的時候也沒有,添加當然也沒什么問題;請注意刪除的效果,雖然,目的 Fragment 確實從 ViewPager 中移除了,但是滑動后面的頁面會發現出現了數據錯亂。
分析一下優化的思路:
先來了解 FragmentPagerAdapter 中是如何管理 Fragment 的,這里涉及到 FragmentPagerAdapter 中的 instantiateItem() 方法:
@Override public Object instantiateItem(ViewGroup container, int position) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } final long itemId = getItemId(position); // Do we already have this fragment? String name = makeFragmentName(container.getId(), itemId); Fragment fragment = mFragmentManager.findFragmentByTag(name); if (fragment != null) { if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); mCurTransaction.attach(fragment); } else { fragment = getItem(position); if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId)); } if (fragment != mCurrentPrimaryItem) { fragment.setMenuVisibility(false); fragment.setUserVisibleHint(false); } return fragment; }
從源碼中可以看到在從 FragmentManager 中取出 Fragment 時調用了 findFragmentByTag() 方法,而這個 Tag 是由 makeFragmentName() 方法生成的。繼續往下可以看到每一個 Fragment 都打上了一個標簽(在 mCurTransaction.add() 方法中)。
也就是說是 FragmentManager 通過 Tag 找相應的 Fragment,從而達到緩存 Fragment 的目的。如果可以找到,就不會創建新的 Fragment,Fragment 的 onCreate()、onCreateView() 等方法都不會再次調用。
那優化的思路就有了:
首先,需要緩存所有 Fragment 的 Tag,代碼如下:
private List<String> mTagList; // 用來存放所有的 Tag // 生成 Tag // 直接從 FragmentPageAdapter 源碼里拷貝 Fragment 生成 Tag 的方法 private String makeFragmentName(int viewId, int index) { return "android:switcher:" + viewId + ":" + index; } // 將 Tag 緩存到 List 中 @Override public Object instantiateItem(ViewGroup container, int position) { mTagList.add(position, makeFragmentName(container.getId(), (int) getItemId(position))); return super.instantiateItem(container, position); }
其次,在更新 Fragment 時,使用相應的 Tag 去 FragmentManamager 中找相應的 Fragment,如果存在,就直接更新,代碼如下:
public void update(int position, String str) { Fragment fragment = mFragmentManager.findFragmentByTag(mTagList.get(position)); if (fragment == null) return; if (fragment instanceof FragmentTest) { ((FragmentTest)fragment).update(str); } notifyDataSetChanged(); }
該方法需要自行在 Fragment 中提供。
最后,對於動態改變 ViewPager 中 Fragment 的數量,如果是添加,那沒什么要注意的;但是刪除有點棘手。
在上面的動態上看到,刪除一個 Fragment 后會出現混亂,這里沒有進一步去研究了,這里僅提供一個示例供參考
public void remove(int position) { mDataList.remove(position); isDataSetChange = true; Fragment fragment = mFragmentManager.findFragmentByTag(mTagList.get(position)); mTagList.remove(position); if (fragment == null) { notifyDataSetChanged(); return; } FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); fragmentTransaction.remove(fragment); fragmentTransaction.commit(); mFragmentManager.executePendingTransactions(); notifyDataSetChanged(); }
注意:
這個”優化“示例,僅僅適用於在只需要更新某個 Fragment 的場景,關於動態刪除 Fragment,該”優化“方案並不適用,也不推薦使用。
四、FragmentStatePagerAdapter
簡介
FragmentStatePagerAdapter 與 FragmentPagerAdapter 類似,這兩個類都繼承自 PagerAdapter。但是,和 FragmentPagerAdapter 不一樣的是,FragmentStatePagerAdapter 只保留當前頁面,當頁面離開視線后,就會被消除,釋放其資源;而在頁面需要顯示時,生成新的頁面(這和 ListView 的實現一樣)。這種方式的好處就是當擁有大量的頁面時,不必在內存中占用大量的內存。
FragmentStatePagerAdapter 的實現與 FragmentPagerAdapter 有很大區別,如果照搬上述 FragmentPagerAdapter 刷新數據的方式,你會發現沒有什么問題。
另一種思路
如果,你也是這樣使用 FragmentStatePagerAdapter 來動態改變 ViewPager 中 Fragment,並且在 remove Fragment 時遇到了 IllegalStateException。那么,你可以考慮使用下面的方式,先看代碼(FSPagerAdapter .java):
public class FSPagerAdapter extends FragmentStatePagerAdapter { private ArrayList<Fragment> mFragmentList; public FSPagerAdapter(FragmentManager fm, List<Integer> types) { super(fm); updateData(types); } public void updateData(List<Integer> dataList) { ArrayList<Fragment> fragments = new ArrayList<>(); for (int i = 0, size = dataList.size(); i < size; i++) { Log.e("FPagerAdapter1", dataList.get(i).toString()); fragments.add(FragmentTest.instance(dataList.get(i))); } setFragmentList(fragments); } private void setFragmentList(ArrayList<Fragment> fragmentList) { if(this.mFragmentList != null){ mFragmentList.clear(); } this.mFragmentList = fragmentList; notifyDataSetChanged(); } @Override public int getCount() { return this.mFragmentList.size(); } public int getItemPosition(Object object) { return POSITION_NONE; } @Override public Fragment getItem(int position) { return mFragmentList.get(position); } }
上面的代碼挺簡單,稍微解釋一下實現思路:
1、緩存所有的 Fragment
使用一個 List 將數據源對應的 Fragment 都緩存起來
2、更新數據源,刷新 Fragment
當有數據源更新的時候,從 List 中取出相應的 Fragment,然后刷新 Adapter
3、刪除數據時,刪除 List 中對應的 Fragment
當數據源中刪除某項時,將 List 中對應的 Fragment 也刪除,然后刷新 Adapter
小結
關於 ViewPager 數據源刷新比較麻煩的地方是從數據源中刪除數據的情況,這和 ViewPager 的實現方式有關,我們在解決該問題的時候要分具體情況來采取不同的方案。
上面提供的方案也不是完美的;這里主要是探討關於 ViewPager 數據源刷新的問題,關於 ViewPager 的詳細使用不是本文重點,這里就不涉及了。