文章目录
一、问题
public class FeedPageAdapter extends FragmentStatePagerAdapter {
@Override
public Fragment getItem(int position) {
return mLists.get(position).fragment;
}
public void initFeedTabs(List<FeedTab> lists) {
mLists.clear();
if (null != lists) {
mLists.addAll(lists);
}
notifyDataSetChanged();
}
}
直接传入了fragments,并且这个fragment在外部也是会复用,我们原意是通过initFeedTabs(…)可以每次更新Viewpager,之所以会有这样的认识,其实很大一部分是因为我们平时使用ListView或者RecyclerView时使用的Adapter的影响,我们会潜意识的认为“调用了notifyDataSetChanged()方法之后,ViewPager就会去调用getItem方法来获取新的Fragment以替换旧的Fragment”,就好像我们使用ListView或者RecyclerView时,它会去回调BaseAdapter的getView方法来获取新的View一样, 实际使用中出现了类似问题:
- 第一个问题:调用notifyDataSetChanged()之后,ViewPager当前存在的页面中的Fragment不会发生变化,即便我们更新了已存在位置的数据,但实际上没变化
- 第二个问题:对于重新添加的界面,不会回调getItem来获取新的Fragment。
甚至在热更新(二次触发)initFeedTabs()时可能引发崩溃,具体的问题场景如下:
- ABC-ABDC崩溃
- ABC-ABCD正常
- ABCD-ABDC不崩溃但是错乱
- ABCD-ABC正常
二、源码分析
我们调用PagerAdapter#notifyDataSetChanged(),最终会走到ViewPager#dataSetChanged方法
2.1ViewPager#dataSetChanged
我们关注下这个方法:
void dataSetChanged() {
//当前的个数
final int adapterCount = mAdapter.getCount();
mExpectedAdapterCount = adapterCount;
//此时为true————这里判断了老的缓存个数和新的个数的差异
boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1
&& mItems.size() < adapterCount;
int newCurrItem = mCurItem;
boolean isUpdating = false;
//遍历列表,这个
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
//这里是关键,默认都是返回POSITION_UNCHANGED
final int newPos = mAdapter.getItemPosition(ii.object);
//第一种情况:如果返回的是POSITION_UNCHANGED,那么表示这个界面在ViewPager中的位置没有变,那么不需要更新.
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}
//第二种情况:如果返回的是POSITION_NONE,就表示这个界面在ViewPager中不存在了,那么就把它移除.
if (newPos == PagerAdapter.POSITION_NONE) {
mItems.remove(i);
i--;
if (!isUpdating) {
mAdapter.startUpdate(this);
isUpdating = true;
}
mAdapter.destroyItem(this, ii.position, ii.object);
needPopulate = true;
if (mCurItem == ii.position) {
// Keep the current item in the valid range
newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
needPopulate = true;
}
continue;
}
//第三种情况:界面仍然存在,但是其在ViewPager中的位置发生了改变.
if (ii.position != newPos) {
if (ii.position == mCurItem) {
// Our current item changed position. Follow it.
newCurrItem = newPos;
}
ii.position = newPos;
needPopulate = true;
}
}
if (isUpdating) {
mAdapter.finishUpdate(this);
}
Collections.sort(mItems, COMPARATOR);
if (needPopulate) {
// Reset our known page widths; populate will recompute them.
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.isDecor) {
lp.widthFactor = 0.f;
}
}
setCurrentItemInternal(newCurrItem, false, true);
requestLayout();
}
}
2.1.1 ArrayList.ViewPager.ItemInfo. mItems
其中有一个很关键的数据 ArrayList<ViewPager.ItemInfo> mItems = new ArrayList();
,这个List存储了当前已经持有的了的ViewPager的ChildPage的初始化信息,可以理解为是Viewpager级别的缓存标示
static class ItemInfo {
Object object; //通过PagerAdapter#instantiateItem所返回的Object.
int position; //这个Item所处的位置,也就是上面我们所说的index.
boolean scrolling;
float widthFactor;
float offset;
}
此时,也就是我们位于index=0的页面,mItems的内容为:
[图 items1]
而如果我们滑动到index=2的页面,那么mItems的内容变为:
[图 items2]
2.1.2 Adapter.getItemPosition(ii.object)
我们注意dataSetChanged有一句关键的代码:
final int newPos = mAdapter.getItemPosition(ii.object);
对于它的返回值,有三种处理方式:
- PagerAdapter.POSITION_UNCHANGED:这个ItemInfo在整个ViewPager的位置没有发生改变。
- PagerAdapter.POSITION_NONE:这个ItemInfo在整个ViewPager中已经不存在了。
- 需要调用 mAdapter.destroyItem(this, ii.position, ii.object); 移除
- 需要 needPopulate = true; 重刷Viewpager
- ii.position != newPos,也就是说ItemInfo在ViewPager仍然需要存在,但是它的位置发生了改变。
- 需要 needPopulate = true; 重刷Viewpager
也就是说,notifyDataSetChanged()只处理ViewPager当前已经存在的界面,而对于这些界面如何处理,则要依赖于getItemPosition的返回值,但是FragmentPagerAdapter 和 FragmentStatePagerAdapter的返回值默认是POSITION_UNCHANGED,因此,当前已经存在的界面不会发生任何改变。
2.3 什么时候触发PagerAdapter#instantiateItem
我们上文说了notifyDataSetChanged()只处理ViewPager当前已经存在的界面,其实不是特别准确,因为新界面的初始化其实是依赖于当前选中位置以及mOffscreenPageLimit。
我们注意dataSetChanged有一句关键的代码:
if (needPopulate) {
// Reset our known page widths; populate will recompute them.
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.isDecor) {
lp.widthFactor = 0.f;
}
}
setCurrentItemInternal(newCurrItem, false, true);
requestLayout();
}
这个setCurrentItemInternal(…)其实是可能引发新页面的初始化的,关键在于当前选中位置。 我们看下这个函数
- 真正执行item选中的方法,第一个参数为要选中的item,第二个参数为是否是由顺滑滑动,第三个参数为是否是永久的,第四个参数为滑动速度
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
if (mAdapter == null || mAdapter.getCount() <= 0) {
setScrollingCacheEnabled(false);
return;
}
if (!always && mCurItem == item && mItems.size() != 0) {
setScrollingCacheEnabled(false);
return;
}
if (item < 0) {
item = 0;
} else if (item >= mAdapter.getCount()) {
item = mAdapter.getCount() - 1;
}
final int pageLimit = mOffscreenPageLimit;
if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
// We are doing a jump by more than one page. To avoid
// glitches, we want to keep all current pages in the view
// until the scroll ends.
for (int i = 0; i < mItems.size(); i++) {
mItems.get(i).scrolling = true;
}
}
final boolean dispatchSelected = mCurItem != item;
if (mFirstLayout) {
// We don't have any idea how big we are yet and shouldn't have any pages either.
// Just set things up and let the pending layout handle things.
mCurItem = item;
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
requestLayout();
} else {
populate(item);
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
}
这里其实处理了页面初始化和平滑滚动,其中前者是依赖于populate(item),
2.3.1 populate()
它主要根据制定的页面缓存大小(mOffscreenPageLimit),做了页面的销毁和重建
- populate()几乎把mAdapter的生命周期走了个遍,A-F
- startUpdate()
- getCount()
- instantiateItem()
- destroyItem()
- setPrimaryItem()
- finishUpdate()
- 页数控制和复杂计算,主要做了页面销毁这项工作
void populate(int newCurrentItem) {
...
mAdapter.startUpdate(this); // ------ A
// 0: 设置页数限制,[startPos, endPos]=>[mCurItem - pageLimit, mCurItem + pageLimit]
// 对应 public void setOffscreenPageLimit(int limit);
final int pageLimit = mOffscreenPageLimit;
final int startPos = Math.max(0, mCurItem - pageLimit);
final int N = mAdapter.getCount(); // ------ B
final int endPos = Math.min(N-1, mCurItem + pageLimit);
// 1: Locate the currently focused item or add it if needed.
int curIndex = -1;
ItemInfo curItem = null;
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
final ItemInfo ii = mItems.get(curIndex);
if (ii.position >= mCurItem) { // 1.1: 便利找到第一个大于 mCurItem 的位置
if (ii.position == mCurItem) curItem = ii;
break;
}
}
// 1.2: 由于步骤0 处设置了缓存的页数限制,mItems 中可能会找不到 curItem,
// 需要 addNewItem
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex); // C: addNewItem()里边调用了 mAdapter.instantiateItem()
}
// Fill 3x the available width or up to the number of offscreen
// pages requested to either side, whichever is larger.
// If we have no current item we have no work to do.
// 2: (译)根据 mOffscreenPageLimit 这个参数(默认为1),
// 决定保留的页面范围,即[startPos, endPos]
if (curItem != null) {
// 左边范围
float extraWidthLeft = 0.f;
int itemIndex = curIndex - 1;
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
final int clientWidth = getClientWidth();
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
for (int pos = mCurItem - 1; pos >= 0; pos--) {
// 2.1: 逆序遍历左边,累加 extraWidthLeft,并与 leftWidthNeeded 比较
// 同时,如果 pos 超出边界[startPos, endPos], 则销毁 view
// 这里的参数计算比较复杂,只看了个大概。。。
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
// ------ D
mAdapter.destroyItem(this, pos, ii.object); // 2.2: 回调销毁 view
...
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor; // 2.3: 累加 extraWidthLeft
itemIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor; // 2.4: 累加 extraWidthLeft
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
// 右边情况与左边完全对偶,不再详细贴出
...
// 2.6: 计算页面偏移
calculatePageOffsets(curItem, curIndex, oldCurInfo);
}
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); // ------ E
mAdapter.finishUpdate(this); // ------ F
// 下面两部分分别是 LayoutParams 和 Focus 处理,
// 感觉不太重要,已省略
}
addNewItem()里边调用了 mAdapter.instantiateItem(),,但是只有当前Viewpager缓存的mItems没有对应值的时候(譬如当前位置,或者阀值区域内的),才会触发
ViewPager.ItemInfo addNewItem(int position, int index) {
ViewPager.ItemInfo ii = new ViewPager.ItemInfo();
ii.position = position;
ii.object = this.mAdapter.instantiateItem(this, position);
ii.widthFactor = this.mAdapter.getPageWidth(position);
if (index >= 0 && index < this.mItems.size()) {
this.mItems.add(index, ii);
} else {
this.mItems.add(ii);
}
return ii;
}
需要额外注意的是dataSetChanged函数中,如果getItemPosition返回了POSITION_NONE = -2;,会删除items中的数据,因此会引发重新instantiateItem,这就为下边的结局方案提供了另外一种思路;
if (newPos == -2) {
this.mItems.remove(childCount);
--childCount;
}
这里也印证了,getItemPosition返回了POSITION_NONE = -2就会触发再次instantiateItem。
2.4 FragmentStatePagerAdapter
上述过程中终于说到了PageAdapter, 我们来看下我们使用的FragmentStatePagerAdapter源码:
public abstract class FragmentStatePagerAdapter extends PagerAdapter {
private ArrayList<SavedState> mSavedState = new ArrayList();
private ArrayList<Fragment> mFragments = new ArrayList();
private Fragment mCurrentPrimaryItem = null;
@NonNull
public Object instantiateItem(@NonNull ViewGroup container, int position) {
Fragment fragment;
if (this.mFragments.size() > position) {
fragment = (Fragment)this.mFragments.get(position);
if (fragment != null) {
return fragment;
}
}
if (this.mCurTransaction == null) {
this.mCurTransaction = this.mFragmentManager.beginTransaction();
}
fragment = this.getItem(position);
if (this.mSavedState.size() > position) {
SavedState fss = (SavedState)this.mSavedState.get(position);
if (fss != null) {
fragment.setInitialSavedState(fss);
}
}
while(this.mFragments.size() <= position) {
this.mFragments.add((Object)null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
this.mFragments.set(position, fragment);
this.mCurTransaction.add(container.getId(), fragment);
return fragment;
}
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment)object;
if (this.mCurTransaction == null) {
this.mCurTransaction = this.mFragmentManager.beginTransaction();
}
while(this.mSavedState.size() <= position) {
this.mSavedState.add((Object)null);
}
this.mSavedState.set(position, fragment.isAdded() ? this.mFragmentManager.saveFragmentInstanceState(fragment) : null);
this.mFragments.set(position, (Object)null);
this.mCurTransaction.remove(fragment);
}
}
FragmentStatePagerAdapter 主要是有一个基于位置额缓存列表,这样就解释为:
- ABC-ABDC 可能引发崩溃
- 移动位置的fragmentC,本身是已经added的。虽然在数据源后移一个位置,但是FragmentStatePagerAdapter是有缓存的,同时getitempos返回unchanged导致反而被ViewPager认为原来的位置是ok的(dataSetChanged依赖getitempos来判断是否移除项的,ViewPager持有一个ItemInfoList来备注加了哪些). 点击第四个位置时,由于超出了FragmentStatePagerAdapter的缓存,所以就会再次add这个fragmentC, 崩溃
- 也是基于这个原理,即便我们全改了 getitempos返回none,一样不起作用(这个只是保证了会让Viewpager调用instantiateItem),但是第四个位置,因为超出原缓存,相当于还是会add一个已经added的fragment
- ABC-ABCD正常
- ViewPager中的缓存位置没问题
- FragmentStatePagerAdapter的缓存List没问题
- ABCD-ABDC不崩溃但是错乱
- FragmentStatePagerAdapter的缓存List还是用的之前的
- ABCD-ABC正常
- FragmentStatePagerAdapter的缓存List
解决方案
方案一:不可行
再次更新viewpager时,清除下FragmentStatePagerAdapter的List缓存
/**
* 这个函数不行,因为不仅要去除frament,还要去除ViewPager中的mItems列表,否则当前显示的几个fragment不会被instantiateItem
*/
public void clearFragmentCache(){
if(CheckUtils.checkListIfValid(mLists)){
//FragmentTransaction ft = fragmentManager.beginTransaction();
boolean needChange = false;
for (int i = 0; i < mLists.size(); i++) {
FeedTab feedTab = mLists.get(i);
if(feedTab != null && feedTab.fragment != null && feedTab.fragment.isAdded()){
destroyItem(viewPager, i, feedTab.fragment );
//ft.remove(feedTab.fragment);
needChange = true;
}
}
if(needChange){
//ft.commit();
//ft = null;
// fragmentManager.executePendingTransactions();
finishUpdate(viewPager);
}
}
}
然而实际上是不可行的,我们虽然删除了FragmentStatePagerAdapter的List缓存,但是ViewPager里的mItems还是存在对应位置的数据,,,,这就尴尬了,ViewPager认为之前的位置是存在Page的,但是我们其实手动destroyItem了之前的Page,这就让ViewPage不会在之前的位置再次触发addNewItem,因此之前的位置会是一片空白
方案二:可行
那么有什么方案可以 adapter#destroyItem的同时,删除掉ViewPager中mItems缓存呢,我们查找引用找到了一个方法:
public void setAdapter(PagerAdapter adapter) {
if (mAdapter != null) { // 1: 清空旧的 Adapter, 做一些初始化处理
mAdapter.unregisterDataSetObserver(mObserver);
mAdapter.startUpdate(this);
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
mAdapter.destroyItem(this, ii.position, ii.object);
}
mAdapter.finishUpdate(this);
mItems.clear();
removeNonDecorViews();
mCurItem = 0;
scrollTo(0, 0);
}
// 2: 更新 mAdapter 字段
final PagerAdapter oldAdapter = mAdapter;
mAdapter = adapter;
mExpectedAdapterCount = 0;
// 3: 给 mAdapter 添加数据 mObserver,恢复状态
if (mAdapter != null) {
if (mObserver == null) {
mObserver = new PagerObserver();
}
// 3.1: 给 mAdapter 添加数据 mObserver
mAdapter.registerDataSetObserver(mObserver);
mPopulatePending = false;
final boolean wasFirstLayout = mFirstLayout;
mFirstLayout = true;
mExpectedAdapterCount = mAdapter.getCount();
if (mRestoredCurItem >= 0) { // 3.2: 之前有状态保存下来,恢复状态
mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
setCurrentItemInternal(mRestoredCurItem, false, true);
mRestoredCurItem = -1;
mRestoredAdapterState = null;
mRestoredClassLoader = null;
} else if (!wasFirstLayout) {
// 3.3: 没状态保存,且不是第一次被 Layout 出来 -> populate() 不知道要干嘛。。
populate();
} else { // 3.4: 没状态保存,且是第一次被 Layout 出来 -> 重新布局
requestLayout();
}
}
// 4: 回调监听器
if (mAdapterChangeListener != null && oldAdapter != adapter) {
mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
}
}
- 其中ItemInfo 保存了每一项的信息。然后,mItems其实是页面的缓存,adapter变更的时候要先清空之前缓存
- mRestoredCurItem在onRestoreInstanceState的时候保存了当前选中状态。
- mFirstLayout
- 初始化为true,onLayout()之后变为false,使得在setAdapter()里: 如果已经onLayout()过一次,可以用populate()代替requestLayout()。然后又重置了这个mFirstLayout。
setadapter函数中是有mItems.clear();,因此我们将实现改为:
public void initFeedTabs(List<FeedTab> lists) {
boolean needClearFragmentCache = needClearFragmentCache(lists, mLists);
if(needClearFragmentCache){
viewPager.setAdapter(null);
}
mLists.clear();
if (null != lists) {
mLists.addAll(lists);
}
if(needClearFragmentCache) {
viewPager.setAdapter(this);
}
notifyDataSetChanged();
}
/**
* 1. 原来是null不需要
* 2. 原来和现在的前几个数据一样,不需要
* @param newSource
* @param oldSource
* @return
*/
public boolean needClearFragmentCache(List<FeedTab> newSource, List<FeedTab> oldSource){
boolean result = false;
if(CheckUtils.checkListIfValid(oldSource)){
for (int i = 0; i < oldSource.size(); i++) {
FeedTab oldTab = oldSource.get(i);
if(oldTab != null && CheckUtils.checkListIfValid(newSource, i) && newSource.get(i) != null && TextUtils.equals(oldTab.tabName, newSource.get(i).tabName)){
continue;
}else {
return true;
}
}
}
return result;
}
方案三:可行
上边的方案其实都是基于,在更新数据时全部删除缓存这种思路来实现,然而我们在上述分析中也提到了,Adapter#getItemPost可以引发Viewpager.mItems删除 和 Adapter.destoryItem, 那么另外一种思路就是我们动态改变getItemPostion的返回值
- 对FragmentStatePagerAdapter的源码进行修改,把原来缓存的Fragment List变成缓存我们自己的ItemInfo,ItemInfo中保存了3个数据 :fragment,数据和页面所处的位置。
- 在instantiateItem ,destroyItem 时对List中的ItemInfo进行增加,删除,在getItemPosition时对list中ItemInfo的position按新数据的位置进行赋值。
- 在notifyDataSetChanged后和instantiateItem获取缓存list的排序,增加等调整。
/**
* Created by haofei.yu on 2020/3/25.
*/
public abstract class FixedPagerAdapter2<T> extends PagerAdapter {
private static final String TAG = "FragmentStatePagerAdapt";
private static final boolean DEBUG = false;
private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
private ArrayList<ItemInfo<T>> mItemInfos = new ArrayList();
private Fragment mCurrentPrimaryItem = null;
private boolean mNeedProcessCache = false;
public FixedPagerAdapter2(FragmentManager fm) {
mFragmentManager = fm;
}
/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);
protected Fragment getCachedItem(int position) {
return mItemInfos.size() > position ? mItemInfos.get(position).fragment : null;
}
@Override
public void startUpdate(ViewGroup container) {
if (container.getId() == View.NO_ID) {
throw new IllegalStateException("ViewPager with adapter " + this
+ " requires a view id");
}
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (mItemInfos.size() > position) {
ItemInfo ii = mItemInfos.get(position);
if (ii != null) {
//判断位置是否相等,如果不相等说明新数据有增加或删除(导致了ViewPager那边有空位),
// 而这时notifyDataSetChanged方法还没有完成,ViewPager会先调用instantiateItem来获取新的页面
//所以为了不取错页面,我们需要对缓存进行检查和调整位置:checkProcessCacheChanged
if (ii.position == position) {
return ii;
} else {
checkProcessCacheChanged();
}
}
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
fragment.setInitialSavedState(fss);
}
}
while (mItemInfos.size() <= position) {
mItemInfos.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
ItemInfo<T> iiNew = new ItemInfo<>(fragment, getItemTag(position), position);
mItemInfos.set(position, iiNew);
mCurTransaction.add(container.getId(), fragment);
return iiNew;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
ItemInfo ii = (ItemInfo) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment) object).getView());
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, ii.fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(ii.fragment) : null);
mItemInfos.set(position, null);
mCurTransaction.remove(ii.fragment);
}
@Override
@SuppressWarnings("ReferenceEquality")
public void setPrimaryItem(ViewGroup container, int position, Object object) {
ItemInfo ii = (ItemInfo) object;
Fragment fragment = ii.fragment;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
@Override
public void finishUpdate(ViewGroup container) {
if (mCurTransaction != null) {
mCurTransaction.commitNowAllowingStateLoss();
mCurTransaction = null;
}
}
@Override
public boolean isViewFromObject(View view, Object object) {
Fragment fragment = ((ItemInfo) object).fragment;
return fragment.getView() == view;
}
@Override
public int getItemPosition(Object object) {
mNeedProcessCache = true;
ItemInfo<T> itemInfo = (ItemInfo) object;
int oldPosition = mItemInfos.indexOf(itemInfo);//老数据的老位置
if (oldPosition >= 0) {
T oldTag = itemInfo.data;//老数据的tag
T newTag = getItemTag(oldPosition);//这个位置的新数据的tag
if (tagEquals(oldTag, newTag)) {
return POSITION_UNCHANGED;
} else {
ItemInfo<T> oldItemInfo = mItemInfos.get(oldPosition);
int oldDataNewPosition = getTagPosition(oldTag);//老数据的新位置
if (oldDataNewPosition < 0) {
oldDataNewPosition = POSITION_NONE;//可以删除了
}
//把新的位置赋值到缓存的itemInfo中,以便调整时使用
if (oldItemInfo != null) {
oldItemInfo.position = oldDataNewPosition;
}
return oldDataNewPosition;
}
}
return POSITION_UNCHANGED;
}
@Override
public void notifyDataSetChanged() {
super.notifyDataSetChanged();
//通知ViewPager更新完成后对缓存的ItemInfo List进行调整
checkProcessCacheChanged();
}
private void checkProcessCacheChanged() {
//只有调用过getItemPosition(也就是有notifyDataSetChanged)才进行缓存的调整
if (!mNeedProcessCache) return;
mNeedProcessCache = false;
ArrayList<ItemInfo<T>> pendingItemInfos = new ArrayList<>(mItemInfos.size());
//先存入空数据
for (int i = 0; i < mItemInfos.size(); i++) {
pendingItemInfos.add(null);
}
//根据缓存的itemInfo中的新position把itemInfo入正确的位置
for (ItemInfo<T> itemInfo : mItemInfos) {
if (itemInfo != null) {
if (itemInfo.position >= 0) {
while (pendingItemInfos.size() <= itemInfo.position) {
pendingItemInfos.add(null);
}
pendingItemInfos.set(itemInfo.position, itemInfo);
}
}
}
mItemInfos = pendingItemInfos;
}
@Override
public Parcelable saveState() {
Bundle state = null;
if (mSavedState.size() > 0) {
state = new Bundle();
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
mSavedState.toArray(fss);
state.putParcelableArray("states", fss);
}
for (int i = 0; i < mItemInfos.size(); i++) {
Fragment f = mItemInfos.get(i).fragment;
if (f != null && f.isAdded()) {
if (state == null) {
state = new Bundle();
}
String key = "f" + i;
mFragmentManager.putFragment(state, key, f);
}
}
return state;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle) state;
bundle.setClassLoader(loader);
Parcelable[] fss = bundle.getParcelableArray("states");
mSavedState.clear();
mItemInfos.clear();
if (fss != null) {
for (int i = 0; i < fss.length; i++) {
mSavedState.add((Fragment.SavedState) fss[i]);
}
}
Iterable<String> keys = bundle.keySet();
for (String key : keys) {
if (key.startsWith("f")) {
int index = Integer.parseInt(key.substring(1));
Fragment f = mFragmentManager.getFragment(bundle, key);
if (f != null) {
while (mItemInfos.size() <= index) {
mItemInfos.add(null);
}
f.setMenuVisibility(false);
ItemInfo<T> iiNew = new ItemInfo<>(f, getItemTag(index), index);
mItemInfos.set(index, iiNew);
} else {
Log.w(TAG, "Bad fragment at key " + key);
}
}
}
}
}
protected Fragment getCurrentPrimaryItem() {
return mCurrentPrimaryItem;
}
protected Fragment getFragmentByPosition(int position) {
if (position < 0 || position >= mItemInfos.size()) return null;
return mItemInfos.get(position).fragment;
}
abstract T getItemTag(int position);
abstract boolean tagEquals(T oldData, T newData);
abstract int getTagPosition(T data);
static class ItemInfo<D> {
Fragment fragment;
D data;
int position;
public ItemInfo(Fragment fragment, D data, int position) {
this.fragment = fragment;
this.data = data;
this.position = position;
}
}
}
- 我们通过一个mCurrentItems保存了当前页面中对应的Fragment和其所包含的数据。
- 最重要的是我们重写了getItemPosition方法,根据不同的情况返回位置
- 这里需要子类提供三个方面的信息:
- 新数据在某个位置的数据。
- 某个数据在新数据中的位置。
- 判断两个数据是否相等的标准。