解決ViewPager展示Fragment時重新設定setAdapter不會重置Fragment的bug
用過ListView和RecycleView的人都知道不管當前列表的瀏覽記錄在哪裡,只要重新setAdapter,列表就會重置,即從第一條item開始顯示.
因此,想當然的我也就認為ViewPager也是這個樣子的.結果並不是我想的那麼簡單,重複setAdapter並沒什麼卵用,Fragment的狀態還是上一次所看到的樣子,並沒有重新初始化,Fragment的幾什麼週期方法一個都沒有執行…實在無語…
經除錯後發現ViewPager重新setAdapter並沒有執行FragmentPagerAdapter的getItem方法,因此Adapter中的Fragment並不會重新初始化.
例如我的FragmentPagerAdapter :
class PageAdapter extends FragmentPagerAdapter {
public PageAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
Category c= mData.get(position % mData.size());
return NewsFragment.newInstance (c.id);
}
@Override
public int getCount() {
return mData.size();
}
}
既然getItem方法沒有呼叫,那麼就得看看其父類FragmentPagerAdapter 的原始碼了,看看getItem方法是在哪裡呼叫的.發現是在父類的instantiateItem方法中呼叫的,原始碼如下:
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);//從快取中查詢該fragment
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position); //這裡才會執行getItem方法
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;
}
注意觀察Fragment fragment = mFragmentManager.findFragmentByTag(name);
這句程式碼,這裡FragmentManager是通過tag別名的方式來從快取中查詢對應的Fragment,其tag的命名是有一套命名規範的,是通過ViewPager的id和getItemId的返回值來生成tag.
讀到這裡的時候,你應該就可以明白怎麼做了,如果想要保證setAdapter方法會執行getItem方法,那麼就得保證findFragmentByTag方法返回的Fragment是null的.
如何做到呢?
我這裡想到2種解決思路:
1.重寫FragmentPagerAdapter 的getItemId方法,每次都返回不同的id,這樣就可以保證生成的tag別名每次都是不一樣的,因此也無法從快取中查詢到值
2.清空mFragmentManager內的快取.
清空FragmentManager的快取
我這裡主要介紹的是第2種方法,即清空快取的方式.
首先mFragmentManager,你得明白這個是什麼,這個其實就是你建立FragmentPagerAdapter 的實現類的時候賦值的,一般是通過getSupportFragmentManager
或者getChildFragmentManager
方式得到的值,前者是在Activity中使用,後者是在Fragment中使用.
public abstract class FragmentPagerAdapter extends PagerAdapter {
private static final String TAG = "FragmentPagerAdapter";
private static final boolean DEBUG = false;
private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
private Fragment mCurrentPrimaryItem = null;
public FragmentPagerAdapter(FragmentManager fm) {
mFragmentManager = fm; //這裡賦值
}
....
}
FragmentManager 是一個抽象類,其findFragmentByTag也是抽象的方法
public abstract Fragment findFragmentByTag(String tag);
因此只能找他的實現類了,即FragmentManagerImpl
final class FragmentManagerImpl extends FragmentManager implements LayoutInflater.Factory2 {..}
檢視其findFragmentByTag的具體實現:
@Override
public Fragment findFragmentByTag(String tag) {
if (tag != null) {
// First look through added fragments.
for (int i=mAdded.size()-1; i>=0; i--) { //1
Fragment f = mAdded.get(i);
if (f != null && tag.equals(f.mTag)) {
return f;
}
}
}
if (mActive != null && tag != null) { //2
// Now for any known fragment.
for (int i=mActive.size()-1; i>=0; i--) {
Fragment f = mActive.valueAt(i);
if (f != null && tag.equals(f.mTag)) {
return f;
}
}
}
return null;
}
注意檢視上面標註位置的1和2,分別有2個屬性mAdded和mActive。
findFragmentByTag的操作就是操作這2個東東,那麼這2個東東到底是啥呢,mAdded其實是一個ArrayList集合,而mActive就是一個SparseArray,觀察原始碼便知真相
mAdded和mActive都各自維護了已經載入過的Fragment,也就是這裡所說的快取,既然這樣,我就可以通過反射拿到這2個東東,然後呼叫它們的clear方法來清空快取,那麼findFragmentByTag方法拿到的結果就是null了.
下面看具體的邏輯,在setAdapter方法之前執行下面程式碼:
//先保證ViewPager之前已設定過Adapter,這樣才有可能存在快取
if (mContentVp.getAdapter() != null) {
//獲取FragmentManager實現類的class物件,這裡指的就是FragmentManagerImpl
Class<? extends FragmentManager> aClass = getChildFragmentManager().getClass();
try {
//1.獲取其mAdded欄位
Field f = aClass.getDeclaredField("mAdded");
f.setAccessible(true);
//強轉成ArrayList
ArrayList<Fragment> list = (ArrayList) f.get(getChildFragmentManager());
//清空快取
list.clear();
//2.獲取mActive欄位
f = aClass.getDeclaredField("mActive");
f.setAccessible(true);
//強轉成SparseArray
SparseArray<Fragment> array = (SparseArray) f.get(getChildFragmentManager());
//清空快取
array.clear();
} catch (Exception e) {
e.printStackTrace();
}
}
//再次設定ViewPager的Adapter
mContentVp.setAdapter(new PageAdapter(getChildFragmentManager()));
ok,至此大功告成.