(4.0.23.11)Viewpager与FragmentStatePagerAdapter重刷数据引发的源码分析和原生缺陷解决方案

一、问题

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一样, 实际使用中出现了类似问题:

  1. 第一个问题:调用notifyDataSetChanged()之后,ViewPager当前存在的页面中的Fragment不会发生变化,即便我们更新了已存在位置的数据,但实际上没变化
  2. 第二个问题:对于重新添加的界面,不会回调getItem来获取新的Fragment。

甚至在热更新(二次触发)initFeedTabs()时可能引发崩溃,具体的问题场景如下:

  1. ABC-ABDC崩溃
  2. ABC-ABCD正常
  3. ABCD-ABDC不崩溃但是错乱
  4. 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);

对于它的返回值,有三种处理方式:

  1. PagerAdapter.POSITION_UNCHANGED:这个ItemInfo在整个ViewPager的位置没有发生改变。
  2. PagerAdapter.POSITION_NONE:这个ItemInfo在整个ViewPager中已经不存在了。
    • 需要调用 mAdapter.destroyItem(this, ii.position, ii.object); 移除
    • 需要 needPopulate = true; 重刷Viewpager
  3. 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),做了页面的销毁和重建

  1. populate()几乎把mAdapter的生命周期走了个遍,A-F
    • startUpdate()
    • getCount()
    • instantiateItem()
    • destroyItem()
    • setPrimaryItem()
    • finishUpdate()
  2. 页数控制和复杂计算,主要做了页面销毁这项工作
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 主要是有一个基于位置额缓存列表,这样就解释为:

  1. ABC-ABDC 可能引发崩溃
    • 移动位置的fragmentC,本身是已经added的。虽然在数据源后移一个位置,但是FragmentStatePagerAdapter是有缓存的,同时getitempos返回unchanged导致反而被ViewPager认为原来的位置是ok的(dataSetChanged依赖getitempos来判断是否移除项的,ViewPager持有一个ItemInfoList来备注加了哪些). 点击第四个位置时,由于超出了FragmentStatePagerAdapter的缓存,所以就会再次add这个fragmentC, 崩溃
    • 也是基于这个原理,即便我们全改了 getitempos返回none,一样不起作用(这个只是保证了会让Viewpager调用instantiateItem),但是第四个位置,因为超出原缓存,相当于还是会add一个已经added的fragment
  2. ABC-ABCD正常
    • ViewPager中的缓存位置没问题
    • FragmentStatePagerAdapter的缓存List没问题
  3. ABCD-ABDC不崩溃但是错乱
    • FragmentStatePagerAdapter的缓存List还是用的之前的
  4. 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方法,根据不同的情况返回位置
  • 这里需要子类提供三个方面的信息:
    • 新数据在某个位置的数据。
    • 某个数据在新数据中的位置。
    • 判断两个数据是否相等的标准。

参考文献

猜你喜欢

转载自blog.csdn.net/fei20121106/article/details/106300011