ViewPager刷新問題詳解

ViewPager刷新問題詳解

作者:李旺成

時間:2016年5月3日


一、PagerAdapter介紹

先看效果圖

PageAdapter 使用示例

PagerAdapter簡介

ListView 大家應(yīng)該都很熟悉吧辉川!ListView 一般都需要一個 Adapter 來填充數(shù)據(jù)串绩,如 ArrayAdapter厂财、SimpleAdapter新症。PagerAdapter 就是 ViewPager 的 Adapter筑凫,與 ListView 的 Adapter 作用一樣。

ViewPager->PageAdapter == ListView->BaseAdapter

先看下官方介紹

官方介紹

PageAdapter 類

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ù)源中的某項

更新數(shù)據(jù)測試

對應(yīng)代碼:

private void refresh() {
    mDataList.set(0, "更新數(shù)據(jù)源測試");
    mPagerAdapter.notifyDataSetChanged();
}

問題描述:在演示動畫中可以看到,更新數(shù)據(jù)源之后視圖并沒有立即刷新咐容,多滑動幾次再次回到更新的 Item 時才更新(這里先看問題舆逃,下面會細(xì)說)。

2戳粒、往數(shù)據(jù)源中添加數(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ù)

刪除數(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ù)源清空

清空數(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):

DataSetObserver繼承結(jié)構(gòu)

總算找到你了,就是用紅線框出來的那條符衔,雙擊找前,定位過去。

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

先看效果:

FragmentPagerAdapter數(shù)據(jù)源刷新演示1

實現(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

先看效果:

FragmentPagerAdapter數(shù)據(jù)源刷新演示2

從上面的動圖上可以看到,更新某一個 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數(shù)據(jù)源刷新演示

簡介

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ì)使用不是本文重點,這里就不涉及了负乡。

項目地址

GitHub
個人博客

參考

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ū)別

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抖棘,一起剝皮案震驚了整個濱河市茂腥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌切省,老刑警劉巖础芍,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異数尿,居然都是意外死亡仑性,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門右蹦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诊杆,“玉大人,你說我怎么就攤上這事何陆〕啃冢” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵贷盲,是天一觀的道長淘这。 經(jīng)常有香客問我,道長巩剖,這世上最難降的妖魔是什么铝穷? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮佳魔,結(jié)果婚禮上曙聂,老公的妹妹穿的比我還像新娘。我一直安慰自己鞠鲜,他們只是感情好宁脊,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著贤姆,像睡著了一般榆苞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上霞捡,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天坐漏,我揣著相機(jī)與錄音,去河邊找鬼。 笑死仙畦,一個胖子當(dāng)著我的面吹牛输涕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播慨畸,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼莱坎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寸士?” 一聲冷哼從身側(cè)響起檐什,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎弱卡,沒想到半個月后乃正,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡婶博,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年瓮具,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凡人。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡名党,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出挠轴,到底是詐尸還是另有隱情传睹,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布岸晦,位于F島的核電站欧啤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏启上。R本人自食惡果不足惜邢隧,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碧绞。 院中可真熱鬧府框,春花似錦吱窝、人聲如沸讥邻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兴使。三九已至,卻和暖如春照激,著一層夾襖步出監(jiān)牢的瞬間发魄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留励幼,地道東北人汰寓。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像苹粟,于是被迫代替她去往敵國和親有滑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容