作者:李旺成
時間:2016年5月3日
一、PagerAdapter介紹
先看效果圖
PagerAdapter簡介
ListView 大家應(yīng)該都很熟悉吧辉川!ListView 一般都需要一個 Adapter 來填充數(shù)據(jù)串绩,如 ArrayAdapter厂财、SimpleAdapter新症。PagerAdapter 就是 ViewPager 的 Adapter筑凫,與 ListView 的 Adapter 作用一樣。
ViewPager->PageAdapter == ListView->BaseAdapter
先看下官方介紹
官方介紹
PageAdapter 繼承自 Object吨艇,繼承結(jié)構(gòu)參考意義不大躬它,那老實看文檔。文檔上沒有提供示例代碼东涡,只是說了下要自定義 PageAdapter 需要實現(xiàn)下面四個方法:
-
instantiateItem(ViewGroup container, int position):該方法的功能是創(chuàng)建指定位置的頁面視圖冯吓。適配器有責(zé)任增加即將創(chuàng)建的 View 視圖到這里給定的 container 中,這是為了確保在 finishUpdate(viewGroup) 返回時 this is be done!
返回值:返回一個代表新增視圖頁面的 Object(Key)疮跑,這里沒必要非要返回視圖本身桑谍,也可以這個頁面的其它容器。其實我的理解是可以代表當(dāng)前頁面的任意值祸挪,只要你可以與你增加的 View 一一對應(yīng)即可锣披,比如 position 變量也可以做為 Key - destroyItem(ViewGroup container, int position, Object object):該方法的功能是移除一個給定位置的頁面。適配器有責(zé)任從容器中刪除這個視圖贿条,這是為了確保在 finishUpdate(viewGroup) 返回時視圖能夠被移除
- getCount():返回當(dāng)前有效視圖的數(shù)量
-
isViewFromObject(View view, Object object):該函數(shù)用來判斷 instantiateItem() 函數(shù)所返回來的 Key 與一個頁面視圖是否是代表的同一個視圖(即它倆是否是對應(yīng)的雹仿,對應(yīng)的表示同一個 View)
返回值:如果對應(yīng)的是同一個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("DIY-PageNum-" + dataList.get(position));
container.addView(view);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
});
可以看到實現(xiàn) PagerAdapter 與 BaseAdapter 很類似,只是 PagerAdapter 的 isViewFromObject() 與 instantiateItem() 方法需要好好理解下公黑。這里為了簡化 PagerAdapter 的使用邑商,我做了個簡單的封裝:
public abstract class APagerAdapter<T> extends PagerAdapter {
protected LayoutInflater mInflater;
protected List<T> mDataList;
private SparseArray<View> mViewSparseArray;
public APagerAdapter(Context context, List<T> dataList) {
mInflater = LayoutInflater.from(context);
mDataList = dataList;
mViewSparseArray = new SparseArray<View>(dataList.size());
}
@Override
public int getCount() {
if (mDataList == null) return 0;
return mDataList.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = mViewSparseArray.get(position);
if (view == null) {
view = getView(position);
mViewSparseArray.put(position, view);
}
container.addView(view);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView(mViewSparseArray.get(position));
}
public abstract View getView(int position);
}
APagerAdapter 類模仿 ListView 的 BaseAdapter,抽象出一個 getView() 方法凡蚜,在內(nèi)部使用 SparesArray 緩存所有顯示過的 View人断。這樣使用就很簡單了,繼承 APagerAdapter朝蜘,實現(xiàn) getView() 方法即可(可以參考:DemoPagerAdapter.java)恶迈。
PagerAdapter 刷新的問題
提出問題
在使用 ListView 的時候,我們往往習(xí)慣了更新 Adapter 的數(shù)據(jù)源谱醇,然后調(diào)用 Adapter 的 notifyDataSetChanged() 方法來刷新列表(有沒有點 MVC 的感覺)暇仲。
PagerAdapter 也有 notifyDataSetChanged() 方法,那我們按照這個流程來試試副渴,看有沒有什么問題奈附。(ListView 的示例就不在這里演示了,感興趣的可以自己去試試煮剧,非常簡單)
那么我的問題是:“ViewPager 的 PagerAdapter 在數(shù)據(jù)源更新后斥滤,能否自動刷新視圖讼载?”
帶著問題,我們做一些實驗中跌,下面實驗的思路是:修改數(shù)據(jù)源,然后通知 PagerAdapter 更新菇篡,查看視圖的變化漩符。
實驗環(huán)境準(zhǔn)備
看看實驗環(huán)境,上代碼:
private void initData() {
// 數(shù)據(jù)源
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);
}
});
}
ViewPager 的 Item:item_vp_demopageradapter.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<ImageView
android:id="@+id/iv_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_horizontal_margin"
android:src="@mipmap/ic_launcher" />
<!-- 用于顯示文本驱还,數(shù)據(jù)更新體現(xiàn)在這里 -->
<TextView
android:id="@+id/tv_pagenum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_horizontal_margin"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="DIY-Page-" />
</LinearLayout>
很簡單的代碼嗜暴,并且加了注釋,直接往下看實驗议蟆。
PagerAdapter 刷新實驗
1闷沥、更新數(shù)據(jù)源中的某項
對應(yīng)代碼:
private void refresh() {
mDataList.set(0, "更新數(shù)據(jù)源測試");
mPagerAdapter.notifyDataSetChanged();
}
問題描述:在演示動畫中可以看到,更新數(shù)據(jù)源之后視圖并沒有立即刷新咐容,多滑動幾次再次回到更新的 Item 時才更新(這里先看問題舆逃,下面會細(xì)說)。
2戳粒、往數(shù)據(jù)源中添加數(shù)據(jù)
對應(yīng)代碼:
private void add() {
mDataList.add("這是新添加的Item");
mPagerAdapter.notifyDataSetChanged();
}
問題描述:沒什么問題路狮,數(shù)據(jù)源添加數(shù)據(jù)后通知 PagerAdapter 刷新,ViewPager 中就多了一個 Item蔚约。
3奄妨、從數(shù)據(jù)源中刪除數(shù)據(jù)
private void delete() {
mDataList.remove(0);
mPagerAdapter.notifyDataSetChanged();
}
問題描述:這個問題就較多了,首先苹祟,如果是刪除當(dāng)前 Item砸抛,那么會看到?jīng)]有任何反應(yīng);其次树枫,如果刪除的不是當(dāng)前 Item直焙,會發(fā)現(xiàn)出現(xiàn)了數(shù)據(jù)錯亂,并且后面有 Item 滑不過去砂轻,但是按住往后滑的時候可以看到后面的 Item箕般。
4、將數(shù)據(jù)源清空
private void clean() {
mDataList.clear();
mPagerAdapter.notifyDataSetChanged();
}
問題描述:從上面的動圖可以看到舔清,清空數(shù)據(jù)源之后丝里,會殘留一個 Item。
說明:先不要計較上面所寫的 PagerAdapter 是否有問題体谒,這里只是想引出問題來杯聚,下面會針對 PagerAdapter、FragmentPagerAdapter 以及 FragmentStatePagerAdapter 來分析問題原因和給出解決方案抒痒。
二幌绍、PagerAdapter
從上面的實驗可以看出 ViewPager 不同于 ListView,如果單純的調(diào)用 ViewPager.getAdapter().notifyDataSetChanged() 方法(即 PagerAdapter 的 notifyDataSetChanged()方法)頁面并沒有刷新。
PagerAdapter 用于 ViewPager 的 Item 為普通 View的情況傀广,這個相對簡單颁独,所以最先介紹。
相信很多同學(xué)都搜過類似的問題 —— “PagerAdapter 的 notifyDataSetChanged() 不刷新伪冰?”誓酒。有的說這是 bug,有的則認(rèn)為 Google 是特意這樣設(shè)計的贮聂,個人傾向后一種觀點(我覺得這是 Google 為了 ViewPager 性能考慮而設(shè)計的靠柑,畢竟 ViewPager 需要顯示“很多大的”視圖,而且要防止用戶滑動時覺得卡頓)吓懈。
ViewPager 刷新分析
先來了解下 ViewPager 的刷新過程:
1歼冰、刷新的起始
ViewPager 的刷新是從調(diào)用其 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()
上面的方法中出現(xiàn)了兩個關(guān)鍵的成員變量:
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畔勤,通知 變化發(fā)生了的代碼。代碼很簡單扒磁,那關(guān)鍵是這個 mObservers 包含哪些 Observer 呢庆揪?
3、DataSetObserver
直接從 mObservers 點進(jìn)去你會發(fā)現(xiàn)這個:
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 中 將光標(biāo)放到類名上均澳,按 F4):
總算找到你了,就是用紅線框出來的那條符衔,雙擊找前,定位過去。
4判族、PagerObserver 內(nèi)部類
PagerObserver 是 ViewPager 中的一個內(nèi)部類躺盛,實現(xiàn)也很簡單,就是調(diào)用了 ViewPager 中的 dataSetChanged() 方法形帮,真正的關(guān)鍵來了槽惫。
private class PagerObserver extends DataSetObserver {
@Override
public void onChanged() {
dataSetChanged();
}
@Override
public void onInvalidated() {
dataSetChanged();
}
}
5周叮、ViewPager 的 dataSetChanged()
這個方法的實現(xiàn)較長,里面的邏輯看上去挺復(fù)雜的界斜,這里就不展示全部的源碼了仿耽,列下關(guān)鍵點:
...
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 循環(huán)里面有兩個 continue 語句,這可能是比較關(guān)鍵的代碼各薇,幸好不用我們繼續(xù)深入了项贺,官方給出了解釋:
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 的位置如果沒有發(fā)生變化,則返回 POSITION_UNCHANGED得糜。如果返回了 POSITION_NONE,表示該位置的 Item 已經(jīng)不存在了晰洒。默認(rèn)的實現(xiàn)是假設(shè) Item 的位置永遠(yuǎn)不會發(fā)生變化朝抖,而返回 POSITION_UNCHANGED。(參考自:追溯源碼解決android疑難有關(guān)問題1-Viewpager之notifyDataSetChanged無刷新)
上面在源碼里面跟了一大圈是不是還是感覺沒有明朗谍珊,因為還有一個很關(guān)鍵的類 —— PagerAdapter 沒有介紹治宣,再給點耐心矾克,繼續(xù)阐滩。
6羡微、PagerAdapter 的工作流程
其實就是 PagerAdapter 中方法的執(zhí)行順序围肥,來看看 Leo8573 的分析(個人感覺基本說到位了简烘,所以直接拷過來了):
PagerAdapter 作為 ViewPager 的適配器缰泡,無論 ViewPager 有多少頁跌帐,PagerAdapter 在初始化時也只初始化開始的2個 View谦纱,即調(diào)用2次instantiateItem 方法打掘。而接下來每當(dāng) ViewPager 滑動時华畏,PagerAdapter 都會調(diào)用 destroyItem 方法將距離該頁2個步幅以上的那個 View 銷毀,以此保證 PagerAdapter 最多只管轄3個 View尊蚁,且當(dāng)前 View 是3個中的中間一個亡笑,如果當(dāng)前 View 缺少兩邊的 View,那么就 instantiateItem横朋,如里有超過2個步幅的就 destroyItem仑乌。
簡易圖示:
*
------+---+---+---+------
... 0 | 1 | 2 | 3 | 4 ...
------+---+---+---+------
當(dāng)前 View 為2號 View,所以 PagerAdapter 管轄1琴锭、2晰甚、3三個 View,接下來向左滑動-->
*
------+---+---+---+------
... 1 | 2 | 3 | 4 | 5 ...
------+---+---+---+------
滑動后决帖,當(dāng)前 View 變?yōu)?號 View压汪,PagerAdapter 會 destroyItem 0號View,instantiateItem 5號 View古瓤,所以 PagerAdapter 管轄2止剖、3腺阳、4三個 View。(參考自:關(guān)于ViewPager的數(shù)據(jù)更新問題小結(jié))
總結(jié)一下: Viewpager 的刷新過程是這樣的穿香,在每次調(diào)用 PagerAdapter 的 notifyDataSetChanged() 方法時亭引,都會激活 getItemPosition(Object object) 方法,該方法會遍歷 ViewPager 的所有 Item(由緩存的 Item 數(shù)量決定皮获,默認(rèn)為當(dāng)前頁和其左右加起來共3頁焙蚓,這個可以自行設(shè)定,但是至少會緩存2頁)洒宝,為每個 Item 返回一個狀態(tài)值(POSITION_NONE/POSITION_UNCHANGED)购公,如果是 POSITION_NONE,那么該 Item 會被 destroyItem(ViewGroup container, int position, Object object) 方法 remove 掉雁歌,然后重新加載宏浩,如果是 POSITION_UNCHANGED,就不會重新加載靠瞎,默認(rèn)是 POSITION_UNCHANGED比庄,所以如果不重寫 getItemPosition(Object object),修改返回值乏盐,就無法看到 notifyDataSetChanged() 的刷新效果佳窑。
最簡單的解決方案
那就是直接一刀切:重寫 PagerAdapter 的 getItemPosition(Object object) 方法,將返回值固定為 POSITION_NONE父能。
先看看效果:
![最簡單解決方案示例](http://upload-images.jianshu.io/upload_images/1233754-0071612440ec3200.gif?imageMogr2/auto-orient/strip ”最簡單解決方案示例“)
上代碼(PagerAdapterActivity.java):
@Override
public int getItemPosition(Object object) {
// 最簡單解決 notifyDataSetChanged() 頁面不刷新問題的方法
return POSITION_NONE;
}
該方案的缺點:有個很明顯的缺陷神凑,那就是會刷新所有的 Item,這將導(dǎo)致系統(tǒng)資源的浪費何吝,所以這種方式不適合數(shù)據(jù)量較大的場景耙厚。
注意:
這種方式還有一個需要注意的地方,就是重寫 destoryItem() 方法:
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
// 把 Object 強(qiáng)轉(zhuǎn)為 View岔霸,然后將 view 從 ViewGroup 中清除
container.removeView((View) object);
}
最簡方案的優(yōu)化
這里提供一個思路薛躬,畢竟場景太多,相信大家理解了思路要實現(xiàn)就很簡單了呆细,閑話不多說型宝。
思路:在 instantiateItem() 方法中給每個 View 添加 tag(使用 setTag() 方法),然后在 getItemPosition() 方法中通過 View.getTag() 來判斷是否是需要刷新的頁面絮爷,是就返回 POSITION_NONE趴酣,否就返回 POSITION_UNCHANGED。 (參考自:ViewPager刷新單個頁面的方法)
注意:這里有一點要注意的是坑夯,當(dāng)清空數(shù)據(jù)源的時候需要返回 POSITION_NONE岖寞,可用如下代碼:
if (mDataList != null && mDataList.size()==0) {
return POSITION_NONE;
}
關(guān)于 PagerAdapter 的介紹就到這里了,雖然 FragmentPagerAdapter 與 FragmentStatePagerAdapter 都是繼承自 PagerAdapter柜蜈。但是仗谆,這兩個是專門為以 Fragment 為 Item 的 ViewPager 所準(zhǔn)備的指巡,所以有其特殊性。且看下面的介紹隶垮。
三藻雪、FragmentPagerAdapter
簡介
上面通過使 getItemPosition() 方法返回 POSITION_NONE 到達(dá)數(shù)據(jù)源變化(也就是調(diào)用 notifyDataSetChanged())時,刷新視圖的目的狸吞。但是當(dāng)我們使用 Fragment 作為 ViewPager 的 Item 時勉耀,就需要多考慮一些了,而且一般是使用 FragmentPagerAdapter 或者 FragmentStatePagerAdapter蹋偏。
這里不展開討論 FragmentPagerAdapter 與 FragmentStatePagerAdapter 的異同和使用場景了便斥,感興趣的可以看看這篇文章:FragmentPagerAdapter與FragmentStatePagerAdapter區(qū)別。
下面先來看看使用 FragmentPagerAdapter 時威始,如何在數(shù)據(jù)源發(fā)生變化時枢纠,刷新 Fragment 或者動態(tài)改變 Items 的數(shù)量。
方案:清除 FragmentManager 中緩存的 Fragment
先看效果:
實現(xiàn)上圖效果的關(guān)鍵代碼:
1字逗、FPagerAdapter1Activity.java
private void refresh() {
if (checkData()) return;
mDataList.set(0, 7); // 修改數(shù)據(jù)源
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宅广、思路分析
上面的代碼思路很簡單葫掉,就是當(dāng)數(shù)據(jù)源發(fā)生變化時,先將 FragmentManger 里面所有緩存的 Fragment 全部清除跟狱,然后重新創(chuàng)建俭厚,這樣達(dá)到刷新視圖的目的。
但是驶臊,這樣做有一個缺點挪挤,那就是會造成不必要的浪費,會影響性能关翎。還有就是必須使用一個 List 緩存所有的 Fragment扛门,這也得占用不少內(nèi)存...
思路挺簡單,這里不再贅述纵寝,那看看有沒有什么可以優(yōu)化的论寨。
優(yōu)化:通過 Tag 獲取緩存的 Fragment
先看效果:
從上面的動圖上可以看到,更新某一個 Fragment 沒有問題爽茴,清空數(shù)據(jù)源的時候也沒有葬凳,添加當(dāng)然也沒什么問題;請注意刪除的效果室奏,雖然火焰,目的 Fragment 確實從 ViewPager 中移除了,但是滑動后面的頁面會發(fā)現(xiàn)出現(xiàn)了數(shù)據(jù)錯亂胧沫。
分析一下優(yōu)化的思路:
先來了解 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 時調(diào)用了 findFragmentByTag() 方法占业,而這個 Tag 是由 makeFragmentName() 方法生成的。繼續(xù)往下可以看到每一個 Fragment 都打上了一個標(biāo)簽(在 mCurTransaction.add() 方法中)江场。
也就是說是 FragmentManager 通過 Tag 找相應(yīng)的 Fragment纺酸,從而達(dá)到緩存 Fragment 的目的。如果可以找到址否,就不會創(chuàng)建新的 Fragment餐蔬,F(xiàn)ragment 的 onCreate()、onCreateView() 等方法都不會再次調(diào)用佑附。
那優(yōu)化的思路就有了:
首先樊诺,需要緩存所有 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 時词爬,使用相應(yīng)的 Tag 去 FragmentManamager 中找相應(yīng)的 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 中提供。
最后叽赊,對于動態(tài)改變 ViewPager 中 Fragment 的數(shù)量恋沃,如果是添加,那沒什么要注意的必指;但是刪除有點棘手囊咏。
在上面的動態(tài)上看到,刪除一個 Fragment 后會出現(xiàn)混亂塔橡,這里沒有進(jìn)一步去研究了梅割,這里僅提供一個示例供參考(這個示例代碼有問題,僅供參考)
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();
}
注意:
這個”優(yōu)化“示例葛家,僅僅適用于在只需要更新某個 Fragment 的場景户辞,關(guān)于動態(tài)刪除 Fragment,該”優(yōu)化“方案并不適用癞谒,也不推薦使用底燎。
四、FragmentStatePagerAdapter
先看效果:
簡介
FragmentStatePagerAdapter 與 FragmentPagerAdapter 類似扯俱,這兩個類都繼承自 PagerAdapter书蚪。但是,和 FragmentPagerAdapter 不一樣的是迅栅,F(xiàn)ragmentStatePagerAdapter 只保留當(dāng)前頁面殊校,當(dāng)頁面離開視線后,就會被消除读存,釋放其資源为流;而在頁面需要顯示時呕屎,生成新的頁面(這和 ListView 的實現(xiàn)一樣)。這種方式的好處就是當(dāng)擁有大量的頁面時敬察,不必在內(nèi)存中占用大量的內(nèi)存秀睛。(參考自:FragmentPagerAdapter與FragmentStatePagerAdapter區(qū)別)
FragmentStatePagerAdapter 的實現(xiàn)與 FragmentPagerAdapter 有很大區(qū)別,如果照搬上述 FragmentPagerAdapter 刷新數(shù)據(jù)的方式莲祸,你會發(fā)現(xiàn)沒有什么問題(可以使用 FPagerAdapter11.java 測試)蹂安。
另一種思路
但是,我在項目中實際應(yīng)用的時候(Fragment 比較復(fù)雜锐帜,里面有網(wǎng)絡(luò)任務(wù)等)出現(xiàn)了 IllegalStateException田盈,發(fā)生在 ”fragmentTransaction.remove(f);“ 時。當(dāng)時找了一些文章沒有解決該問題缴阎,考慮到項目中的 Fragment 里面邏輯過多允瞧,就換思路,沒有在這個上面繼續(xù)深究了蛮拔。
如果述暂,你也是這樣使用 FragmentStatePagerAdapter 來動態(tài)改變 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);
}
}
對應(yīng)的測試 Activity 見 FSPagerAdapterActivity.java踱卵。
上面的代碼挺簡單廊驼,稍微解釋一下實現(xiàn)思路:
1据过、緩存所有的 Fragment
使用一個 List 將數(shù)據(jù)源對應(yīng)的 Fragment 都緩存起來
2惋砂、更新數(shù)據(jù)源,刷新 Fragment
當(dāng)有數(shù)據(jù)源更新的時候绳锅,從 List 中取出相應(yīng)的 Fragment西饵,然后刷新 Adapter
3、刪除數(shù)據(jù)時鳞芙,刪除 List 中對應(yīng)的 Fragment
當(dāng)數(shù)據(jù)源中刪除某項時眷柔,將 List 中對應(yīng)的 Fragment 也刪除,然后刷新 Adapter
小結(jié)
關(guān)于 ViewPager 數(shù)據(jù)源刷新比較麻煩的地方是從數(shù)據(jù)源中刪除數(shù)據(jù)的情況原朝,這和 ViewPager 的實現(xiàn)方式有關(guān)驯嘱,我們在解決該問題的時候要分具體情況來采取不同的方案。
上面提供的方案也不是完美的喳坠,還有很多不足鞠评,如果你在應(yīng)用的過程中遇到了問題,那么請反饋給我壕鹉,大家一起完善剃幌。
這里主要是探討關(guān)于 ViewPager 數(shù)據(jù)源刷新的問題聋涨,關(guān)于 ViewPager 的詳細(xì)使用不是本文重點,這里就不涉及了负乡。
項目地址
參考
ViewPager 詳解(二)---詳解四大函數(shù)
pagerAdapter arrayList 數(shù)據(jù)清空,Item 不銷毀的bug解決
ViewPager刷新單個頁面的方法
ViewPager動態(tài)加載牍白、刪除頁面
ViewPager+Fragment滑動界面,并做延遲加載【新版】
關(guān)于ViewPager的數(shù)據(jù)更新問題小結(jié)
Viewpager+fragment數(shù)據(jù)更新問題解析
追溯源碼解決android疑難有關(guān)問題1-Viewpager之notifyDataSetChanged無刷新
解決fragment+viewpager第二次進(jìn)入的時候沒有數(shù)據(jù)的問題
FragmentPagerAdapter刷新fragment最完美解決方案
Viewpager+fragment數(shù)據(jù)更新問題解析
FragmentPagerAdapter與FragmentStatePagerAdapter區(qū)別