1. 程式人生 > >Android-FragmentPagerAdapter重新整理無效的解決方案

Android-FragmentPagerAdapter重新整理無效的解決方案

最近在重構專案的時候有個地方想要做一個更換FragmentPagerAdapter中的Fragment的功能,按照通常使用ListView的習慣做法,如果你只是更新儲存Fragment的List資料,然後呼叫adapter的notifyDataSetChanged()是不會起作用的(下面會分析原因)。
搜尋了下發現此問題普遍存在,多數是說先移除Fragment再notifyDataSetChanged(),因為FragmentPagerAdapter內部會快取Fragment,但是經測試發現僅僅這樣幹是不行的。於是經過一番折騰,參考了各種方案之後我整理了一個可行的方案,本文做一個記錄,以便後續參考,也方便各位道友參考。

下面來分析一下此問題的主要原因:

這可能是Android一個BUG, 與此問題相關的主要有兩個方法:

  • getItemPosition()
  • instantiateItem()

搞清楚這兩個方法的作用基本就知道如何解決了,先來看第一個方法
getItemPosition()

這個是在PagerAdapter中的getItemPosition()原始碼的說明,從它的英文註釋我們可以清楚的知道,這個方法的返回值的意思是:如果給定的item的position沒有發生改變,那麼就返回POSITION_UNCHANGED, 如果給定的item在adapter當中指定位置不再呈現了,那麼就返回POSITION_NONE。預設返回的是POSITION_UNCHANGED

OK, 導致這個問題的一個主要原因我們已經知道了,所以,預設我們是要重寫這個方法的,不然總是返回POSITION_UNCHANGED,那當然是不會更新的了

其實在使用viewpager包含普通view介面的時候我們應該會經常遇到這個問題的。那麼, 這個問題的解決思路就有了:
我們就按照它要求的意思來實現當position發生變化的item我們都返回POSITION_NONE,而position沒有發生變化的item我們就返回POSITION_UNCHANGED

那怎麼來實現呢,我們簡單來想一下,首先我要記錄更新之前的每個item對應的position,然後在更新Fragment列表資料之後,我們再把當前的每一個item的position跟之前的去比對一遍,這樣我們就能知道到底哪個item的position發生了變化,哪個的position依然沒變了。當然前提是比對的item是相同的item, 如果更新之後item都不存在了,那自然要返回POSITION_NONE了。
好,我們這裡就簡單的思路設想一下,後面我會給出完整程式碼。

到這裡包含普通view的viewpager的adapter重新整理問題應該可以解決了,注意,這裡很多人的暴力做法是在getItemPosition()當中直接返回POSITION_NONE,這樣不是不可以,只不過這樣做會預設把所有的view都重新銷燬重建,那肯定不是我想要的理想的情況。

接下來再看另一個方法:
instantiateItem()
這個是在FragmentPagerAdapter的原始碼當中的,可以看到在instantiateItem()方法的內部,它是這樣做的:根據tag查詢對應的Fragment, 如果找到,那麼就通過當前的Transaction進行attach操作,這個fragment就會顯示了,如果沒有找到呢,就去getItem()從你的Fragment列表中獲取一個然後Transaction進行add操作。

所以看到這裡就恍然大悟了,為啥我list裡面的fragment都換了新的了但就是死活不重新整理呢,罪魁禍首就在這裡了,只要它能findFragmentByTag找得到那麼就不會用你的列表中的fragment, 還是用之前的。

那麼,到這裡首先想到的就是,我們在更換或者刪除列表中對應的Fragment時,同時也要將該Fragment從Transaction當中移除,這樣就能夠確保在重新整理資料時adpater會從我們更新後的list中去獲取fragment而不是用之前快取的。

是不是這樣?對不對?嗯,應該是沒有問題的,好,想到這裡那麼我們就可以擼起袖子動手幹了,加上前面getItemPosition()的思路,應該是能夠解決問題的了。假設你按照前面的思路完善了FragmentPagerAdapter的程式碼並準備測試(或者你可以直接往下拖檢視完整的程式碼),你會悲劇的發現,在更換某一個fragment的時候是沒有問題的,但是在刪除某一個fragment時是會出現問題的,會發生crash! 丟擲如下異常:
這裡寫圖片描述

這裡寫圖片描述

哎,沒辦法,江湖就是如此險惡,到處都是坑。。
那麼究竟為什麼發生crash呢,如果你檢視該crash異常棧,我們可以在原始碼中搜素一下找到:

這裡寫圖片描述
沒錯,就是在高亮的這一行,如果你按照前面介紹的方法寫好FragmentPagerAdapter 執行測試了,你就會發現丟擲”Can’t change tag of fragment “的異常,我們可以發現上述的異常是在beginTransaction()之後進行add操作發生的,異常出現的判斷條件是fragment.mTag != null &&!tag.equals(fragment.mTag),這裡的tag就是add時傳入的tag引數, 而mTag是要新增的frgament的tag, 這說明這個fragment之前被新增過,因為下面一行fragment.mTag = tag;我們知道只有新增過的fragment的mTag才不會為null。

那問題肯定是跟tag有關了,我們回到instantiateItem()方法的原始碼,可以看到不管是add操作還是findFragmentByTag時的tag都是通過一個方法生成的:
這裡寫圖片描述
這裡寫圖片描述
makeFragmentName(), 都是這個方法生成的tag, 而這個方法生成tag的辦法是getItemId()和viewId的組合, viewId應該就是我們的fragment的id了,而getItemId():
這裡寫圖片描述
它預設實現就是簡單的返回position,所以tag是由fragment的id+position組成的。
那我們來分析一下,刪除的時候為啥會出現”Can’t change tag of fragment “的異常,先畫個簡圖:

這裡寫圖片描述
假設初始時我們viewpager當中有4個Fragment分別是A B C D, 那麼按照instantiateItem()原始碼中的tag生成方法,這四個fragment被add之後對應四個fragment中的mTag值應該分別就是:A0、B1、C2、D3(假設就用ABCD代表他們的fragment的id),好,現在我們把B對應的Fragment刪除掉(注意此時我們已經按照前面已發現的解決方案實現了的程式碼):
這裡寫圖片描述
此時列表中只剩下A C D三個Fragment, 那麼前面提到過,此時getItemPpsition()方法我們應該做的是A對應的Fragment返回POSITION_UNCHANGED, 因為A的位置沒有發生變化,而B(已刪除)、 C(移位) 、 D(移位) 三個我們應該返回POSITION_NONE,因此我們的adapter在重新整理的時候重新整理到第二個位置時會再首先去查詢對應tag的Fragment:
這裡寫圖片描述
此時查詢的tag是C1,然而找不到,因為C前面add的tag是C2,所以走else, 在else當中就會從我們的列表中去get第1個item,那取到的自然是C,然後對C進行add操作,這時又會生成C對應的tag傳入add()方法,但是此時,注意了,生成C的tag的方法生成的結果是C1(fragment的id+當前position),分析到這裡你可能發現了,前面我們的C是被add過的,所以之前C的mTag是C2,到了這裡add操作時要變成C1了!所以跟著原始碼走進去自然就符合前面“Can’t change tag of fragment “異常的判斷條件fragment.mTag != null &&!tag.equals(fragment.mTag),我們的C之前的mTag不為空並且C1 != C2,所以中標了!

那麼解決問題的方法,首先想到的是為每一個Fragment設定一個唯一的tag值,但是mTag在Fragment原始碼中是protected的,我們是不能改的。。。所以只能去改生成tag的方法makeFragmentName()了,但是一看這個方法又是private的,又不能改。。。。我TMD…CNM..MMP…好吧,再看,因為makeFragmentName()方法用到了getItemId()的返回值,而getItemId()我們是可以重寫的,所以那去只能改getItemId()方法了:

@Override
public long getItemId(int position) {
    // return position;
    return 我們自定義的可以確定當前item的唯一值;
}

因為前面提到過getItemId()方法預設返回的是position,所以我們這個方法要修改一下,返回一個唯一的值,一個可以標誌這個fragment的唯一值就可以了,這樣在刪除操作position發生變化之後,C的tag值經過makeFragmentName()生成的結果總是C+uniqueId, 所以應該不會有問題了。

好了,至此所有問題思路解決完畢,貼一下完善FragmentPagerAdapter的完整程式碼:

/**
 * 載入顯示Fragment的ViewPagerAdapter基類
 * 提供可以重新整理的方法
 *
 * @author Fly
 * @e-mail [email protected]
 * @time 2018/3/22
 */
public class BaseFragmentPagerAdapter extends FragmentPagerAdapter {
    private List<BaseFragment> mFragmentList;
    private FragmentManager mFragmentManager;
    /**下面兩個值用來儲存Fragment的位置資訊,用以判斷該位置是否需要更新*/
    private SparseArray<String> mFragmentPositionMap;
    private SparseArray<String> mFragmentPositionMapAfterUpdate;

    public BaseFragmentPagerAdapter(FragmentManager fm, List<BaseFragment> fragments) {
        super(fm);
        mFragmentList = fragments;
        mFragmentManager = fm;
        mFragmentList = fragments;
        mFragmentPositionMap = new SparseArray<>();
        mFragmentPositionMapAfterUpdate = new SparseArray<>();
        setFragmentPositionMap();
        setFragmentPositionMapForUpdate();
    }

    /**
     * 儲存更新之前的位置資訊,用<hashCode, position>的鍵值對結構來儲存
     */
    private void setFragmentPositionMap() {
        mFragmentPositionMap.clear();
        for (int i = 0; i < mFragmentList.size(); i++) {
            mFragmentPositionMap.put(Long.valueOf(getItemId(i)).intValue(), String.valueOf(i));
        }
    }

    /**
     * 儲存更新之後的位置資訊,用<hashCode, position>的鍵值對結構來儲存
     */
    private void setFragmentPositionMapForUpdate() {
        mFragmentPositionMapAfterUpdate.clear();
        for (int i = 0; i < mFragmentList.size(); i++) {
            mFragmentPositionMapAfterUpdate.put(Long.valueOf(getItemId(i)).intValue(),  String.valueOf(i));
        }
    }

   /**
    * 在此方法中找到需要更新的位置返回POSITION_NONE,否則返回POSITION_UNCHANGED即可
    */
    @Override
    public int getItemPosition(Object object) {
        int hashCode = object.hashCode();
        //查詢object在更新後的列表中的位置
        String position = mFragmentPositionMapAfterUpdate.get(hashCode);
        //更新後的列表中不存在該object的位置了
        if (position == null) {
            return POSITION_NONE;
        } else {
            //如果更新後的列表中存在該object的位置, 查詢該object之前的位置並判斷位置是否發生了變化
            int size = mFragmentPositionMap.size();
            for (int i = 0; i < size ; i++) {
                int key = mFragmentPositionMap.keyAt(i);
                if (key == hashCode) {
                    String index = mFragmentPositionMap.get(key);
                    if (position.equals(index)) {
                        //位置沒變依然返回POSITION_UNCHANGED
                        return POSITION_UNCHANGED;
                    } else {
                        //位置變了
                        return POSITION_NONE;
                    }
                }
            }
        }
        return POSITION_UNCHANGED;
    }

    /**
     * 將指定的Fragment替換/更新為新的Fragment
     * @param oldFragment 舊Fragment
     * @param newFragment 新Fragment
     */
    public void replaceFragment(BaseFragment oldFragment, BaseFragment newFragment) {
        int position = mFragmentList.indexOf(oldFragment);
        if (position == -1) {
            return;
        }
        //從Transaction移除舊的Fragment
        removeFragmentInternal(oldFragment);
        //替換List中對應的Fragment
        mFragmentList.set(position, newFragment);
        //重新整理Adapter
        notifyItemChanged();
    }

    /**
     * 將指定位置的Fragment替換/更新為新的Fragment,同{@link #replaceFragment(BaseFragment oldFragment, BaseFragment newFragment)}
     * @param position    舊Fragment的位置
     * @param newFragment 新Fragment
     */
    public void replaceFragment(int position, BaseFragment newFragment) {
        BaseFragment oldFragment = mFragmentList.get(position);
        removeFragmentInternal(oldFragment);
        mFragmentList.set(position, newFragment);
        notifyItemChanged();
    }

    /**
     * 移除指定的Fragment
     * @param fragment 目標Fragment
     */
    public void removeFragment(BaseFragment fragment) {
        //先從List中移除
        mFragmentList.remove(fragment);
        //然後從Transaction移除
        removeFragmentInternal(fragment);
        //最後重新整理Adapter
        notifyItemChanged();
    }

    /**
     * 移除指定位置的Fragment,同 {@link #removeFragment(BaseFragment fragment)}
     * @param position
     */
    public void removeFragment(int position) {
        BaseFragment fragment = mFragmentList.get(position);
        //然後從List中移除
        mFragmentList.remove(fragment);
        //先從Transaction移除
        removeFragmentInternal(fragment);
        //最後重新整理Adapter
        notifyItemChanged();
    }

    /**
     * 新增Fragment
     * @param fragment 目標Fragment
     */
    public void addFragment(BaseFragment fragment) {
        mFragmentList.add(fragment);
        notifyItemChanged();
    }

    /**
     * 在指定位置插入一個Fragment
     * @param position 插入位置
     * @param fragment 目標Fragment
     */
    public void insertFragment(int position, BaseFragment fragment) {
        mFragmentList.add(position, fragment);
        notifyItemChanged();
    }

    private void notifyItemChanged() {
        //重新整理之前重新收集位置資訊
        setFragmentPositionMapForUpdate();
        notifyDataSetChanged();
        setFragmentPositionMap();
    }

    /**
     * 從Transaction移除Fragment
     * @param fragment 目標Fragment
     */
    private void removeFragmentInternal(BaseFragment fragment) {
        FragmentTransaction transaction = mFragmentManager.beginTransaction();
        transaction.remove(fragment);
        transaction.commitNow();
    }

    /**
     * 此方法不用position做返回值即可破解fragment tag異常的錯誤
     */
    @Override
    public long getItemId(int position) {
        // 獲取當前資料的hashCode,其實這裡不用hashCode用自定義的可以關聯當前Item物件的唯一值也可以,只要不是直接返回position
        return mFragmentList.get(position).hashCode();
    }

    @Override
    public Fragment getItem(int position) {
        return mFragmentList.get(position);
    }

    @Override
    public int getCount() {
        return mFragmentList.size();
    }

    public List<BaseFragment> getFragments() {
        return mFragmentList;
    }
}

好了,現在這個類可以用來實現Fragment列表中的Fragment替換、刪除、新增等操作了,並且可以實時重新整理adapter, 你可以試驗一下。

測試程式碼:
Activity程式碼

public class TestActivity extends FragmentActivity implements View.OnClickListener {
    List<Fragment> mFragmentList;
    ViewPager mViewPager;
    public BaseFragmentPagerAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mViewPager = findViewById(R.id.vp);
        findViewById(R.id.btn_change).setOnClickListener(this);

        mFragmentList = new ArrayList<>();
        mFragmentList.add(getFg("AAA"));
        mFragmentList.add(getFg("BBB"));
        mFragmentList.add(getFg("CCC"));
        mFragmentList.add(getFg("DDD"));
        mAdapter = new BaseFragmentPagerAdapter(getSupportFragmentManager(), mFragmentList);
        mViewPager.setAdapter(mAdapter);
    }

    private TestFragment getFg(String a){
        TestFragment fragment = new TestFragment();
        fragment.setTest(a);
        return fragment;
    }

    @Override
    public void onClick(View view) {
        TestFragment eee = getFg("EEE");

        //新增
        mAdapter.addFragment(eee);
        //插入
        mAdapter.insertFragment(1, eee);

        //刪除
        mAdapter.removeFragment(1);
        //刪除
        mAdapter.removeFragment(mFragmentList.get(1));

        //替換
        mAdapter.replaceFragment(1, eee);
        //替換
        mAdapter.replaceFragment(mFragmentList.get(0), eee);
    }
}

用到的TestFragment:

public class TestFragment extends Fragment {
    private final static String TAG = TestFragment.class.getSimpleName();
    private String test;
    public View mContentView;

    public void setTest(String test) {
        this.test = test;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.e(TAG, "onCreate:  test = "+test);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        mContentView = inflater.inflate(R.layout.layout_fg, null);
        Log.e(TAG, "onCreateView: test = "+test);
        return mContentView;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        Log.e(TAG, "onViewCreated: test = "+test);
        TextView testText = mContentView.findViewById(R.id.tv_test);
        testText.setText(test);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.e(TAG, "onActivityCreated: test = "+test);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e(TAG, "onDestroy:  test = "+test);
    }

}

佈局檔案:

<?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.support.v4.view.ViewPager
        android:id="@+id/vp"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_gravity="center"
        />
    <Button
        android:id="@+id/btn_change"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="btn_change"
        />
</LinearLayout>

佈局檔案很簡單就是一個viewpager+一個button, 然後我們在Activity當中點選這個button對vp的adapter所使用的fragment列表進行操作,並觀察變化。

注意,封裝的Adapter類提供了新增、插入、刪除、替換幾種方法的過載,可以通過指定的位置或者fragment進行操作,在onClick()中測試時,註釋其他的情況,只測試一種情況即可。

另外,我們在TestFragment中的生命週期方法中添加了Log日誌,以便觀察結果。

執行程式碼測試你會發現,當替換掉列表中的一個Fragment時,左右兩邊的Fragment生命週期是不會被呼叫的。這符合我們的預期,因為替換時左右兩邊的Fragment在viewpager中的位置沒有發生變化,所以它們不會被銷燬重建。

當你刪除或者插入一個Fragment時,當前Fragment後面的Fragment會走重新建立view的生命週期方法,而當前Fragment前面的Fragment不會,這也符合我們的預期,但為啥後面的會重建,而前面的不會?別忘了我們使用的viewpager是有預設預載入當前頁面左右兩邊的view的特性的,所以這個也屬於正常的現象,如果viewpager預載入給你造成了困擾,我們可以通過其它方式來避免,當然這是另外的話題了。

相關推薦

Android-FragmentPagerAdapter重新整理無效解決方案

最近在重構專案的時候有個地方想要做一個更換FragmentPagerAdapter中的Fragment的功能,按照通常使用ListView的習慣做法,如果你只是更新儲存Fragment的List資料,然後呼叫adapter的notifyDataSetChange

Android中設定ListView的item高度無效--解決方案

原文地址:https://blog.csdn.net/zhonglinliu/article/details/54580622   問題:      ListView的使用中,item是在adapter中用來顯示每一個小條目的資

android中actionbar的showAsAction屬性設定為always無效解決方案

晚上剛遇到的這個問題,網上給出的解決辦法有這麼幾種: actionBar所在的activity繼承actionBarActivity。用這個的時候,會報一個主題相關的錯誤,要改的東西很多設定自己的名稱空間:xmlns:app="http://schemas.android

android 當設定Activity狀態列為透明時,鍵盤彈出ScrollView滾動無效解決方案

final View decorView = getWindow().getDecorView(); decorView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGloba

Android短輪詢解決方案——CountDownTimer+Handler

receiver font 網上 adc 開始 success 方法 www 請求 轉載請註明原文地址:http://www.cnblogs.com/ygj0930/p/7657194.html 一:應用場景 在諸如自動售賣機之類的掃碼支付場景中,客戶端

xpath中的ends-with無效解決方案

xpath定位 ends-with xpath中的ends-with() 多測師 xpath定位遇到如下問題://*[ends-with(@id,"多測師")]定位不到以"多測師"結尾的元素

第一篇:安裝Android Studio問題及其解決方案

.com 及其 pla try onf posit blog chmod 提示 ubuntu18.04配置android studio3.2.1環境 1.JDK安裝與配置:https://www.cnblogs.com/yuanbo123/p/5819564.html(按照

Unity Android il2cpp熱更解決方案

1. 簡介 這是Unity Android il2cpp熱更解決方案的Demo(Git地址)的說明。 和現有的熱更解決方案不同的是,他不會引入多餘的語言(只是UnityScript,c#...),對Unity程式設計和編碼沒有任何限制。你可以在預置和場景裡的GameObject上新增任何的Compnent

Android 跑馬燈無效解決辦法

xml檔案: <TextView android:id="@+id/text_vv" android:scrollHorizontally="true" android:marqueeRepeatLimit="marquee_forever" android:

VB.net 圖片重新整理閃爍 解決方案

解決圖片重新整理閃爍可使能DoubleBuffering 將以下程式碼插入 Public Sub EnableDoubleBuffering() ' Set the value of the double-buffering style bits to true.

ORA-00904: 識別符號無效——解決方案

轉自:https://blog.csdn.net/jajavaja/article/details/49122639 建表時列名用雙引號引著(用Navicat工具建表預設是加上雙引號的),java連線時就會報錯ORA-00904:   識別符號無效;把雙引去掉就不會報錯了 原: CREATE

Android開發:最全面、最易懂的Android螢幕適配解決方案

前言 Android的螢幕適配一直以來都在折磨著我們Android開發者,本文將結合: 給你帶來一種全新、全面而邏輯清晰的Android螢幕適配思路,只要你認真閱讀,保證你能解決Android的螢幕適配問題! 目錄 定義 使得某一元

【Layui】關於做了分頁後點擊刪除按鈕無效(或者在任何框架點選一個按鈕無效解決方案

author:咔咔 wechat:fangkangfk   案例:   在ajax拼裝完資料後,怎麼點選刪除都是沒有反應,一直以為是資料拼接錯了,最後才反應過來,使用js拼裝起來的資料屬於未來元素,所以點選是沒有用的   所以使用l

實現離線安裝、配置Android Studio開發環境 解決方案

近期專案需要提供AS的離線開發工具安裝 ,沒錯網上的帖子很多 ,但有問題的也很多,因為越往下做 ,一個個問題接踵而至,不同的嘗試,也發現出一條路子,但還是存在一定侷限,但能將就一下,聽我娓娓道來。 1.開發環境的準備 2.相關配置   開發工具準備: A. 

4種必須知道的Android螢幕自適應解決方案

<?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="height_1_80">6px</dimen><dimen name="height_2_80">12px<

Android ANT 多渠道打包解決方案

<span style="font-size:18px;"><!--Android 分渠道打包步驟--> <!--打包之前請確定--> ANDROID_HOME 環境變數 即ANDROID_SDK的安裝路徑 如:

目前Android最全面、最易懂的Android螢幕適配解決方案

前言 Android的螢幕適配一直以來都在折磨著我們Android開發者,本文將結合: 給你帶來一種全新、全面而邏輯清晰的Android螢幕適配思路,只要你認真閱讀,保證你能解決Android的螢幕適配問題! 目錄 Androi

android 架包衝突解決方案

作為一名剛學android五個月的小白,之前無論學習基礎,還是看一些培訓機構的教學專案,裡面都是用listView來展示資料。 下午心血來潮,便想學習一下recycleView,則需要匯入一些其他人的

使用Charles進行HTTPS抓包(包括安裝信任證書以及抓包 出現無法抓包unknown和證書無效解決方案

背景: 在進行App測試或定位線上問題時,經常會遇到抓取HTTPS資料包的需求。一般在windows上會使用fiddler,Mac上使用Charles。對於https請求,抓到的資料因為經過了加密,只能看到亂碼。 本文介紹如何使用Charles來抓取https網路報文

android.os.NetworkOnMainThreadException的解決方案

首先,確定AndroidManifest.xml中 <uses-permission android:name="android.permission.INTERNET" /> 其次,這次異常的丟擲是因為有一個網路操作試圖佔用主執行緒,我們建立一個新執