Android-FragmentPagerAdapter重新整理無效的解決方案
最近在重構專案的時候有個地方想要做一個更換FragmentPagerAdapter中的Fragment的功能,按照通常使用ListView的習慣做法,如果你只是更新儲存Fragment的List資料,然後呼叫adapter的notifyDataSetChanged()是不會起作用的(下面會分析原因)。
搜尋了下發現此問題普遍存在,多數是說先移除Fragment再notifyDataSetChanged(),因為FragmentPagerAdapter內部會快取Fragment,但是經測試發現僅僅這樣幹是不行的。於是經過一番折騰,參考了各種方案之後我整理了一個可行的方案,本文做一個記錄,以便後續參考,也方便各位道友參考。
下面來分析一下此問題的主要原因:
這可能是Android一個BUG, 與此問題相關的主要有兩個方法:
- getItemPosition()
- instantiateItem()
搞清楚這兩個方法的作用基本就知道如何解決了,先來看第一個方法
這個是在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都重新銷燬重建,那肯定不是我想要的理想的情況。
接下來再看另一個方法:
這個是在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" /> 其次,這次異常的丟擲是因為有一個網路操作試圖佔用主執行緒,我們建立一個新執