Fragment 知識梳理(3) FragmentPagerAdapter 和 FragmentStatePagerAdapter 的數

澤毛發表於2017-12-13

一、概述

在上一篇文章中,我們通過原始碼的角度瞭解FragmentPagerAdapterFragmentStatePagerAdapter的原理。這其實是為我們分析資料更新問題做一個鋪墊。 在實際的開發當中,我們在ViewPager中巢狀的Fragment中並不是固定不變的,需要動態地新增和刪除,下面我們就從幾個大家經常會遇到的問題入手,然後分析問題的原因,最後我們嘗試總結一種比較好的資料更新方式。

二、使用FragmentPagerAdapter

2.1 一段有問題的程式碼

public class DemoActivity extends AppCompatActivity {

    private static final int INCREASE = 4;
    private FPAdapter mFPAdapter;
    private List<String> mTitles;
    private int mGroup = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_not_update);
        TextView updateTv = (TextView) findViewById(R.id.tv_update);
        updateTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                updateFragments();
            }
        });
        initFPAFragments();
    }

    private void initFPAFragments() {
        mTitles = new ArrayList<>();
        for (int i = 0; i < INCREASE; i++) {
            mTitles.add("index=" + i + ",group=0");
        }
        ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
        mFPAdapter = new FPAdapter(getSupportFragmentManager(), mTitles);
        viewPager.setAdapter(mFPAdapter);
    }

    private void updateFragments() {
        mTitles.clear();
        mGroup++;
        for (int i = 0; i < INCREASE; i++) {
            mTitles.add("index=" + i + ",group=" + mGroup);
        }
        mFPAdapter.notifyDataSetChanged();
    }

    private class FPAdapter extends FragmentPagerAdapter {

        private List<String> mTitles;

        public FPAdapter(FragmentManager fm, List<String> titles) {
            super(fm);
            mTitles = titles;
        }

        @Override
        public Fragment getItem(int position) {
            Log.d("LogcatFragment", "get Item from FPAdapter, position=" + position);
            return LogcatFragment.newInstance(mTitles.get(position));
        }

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

    }

}
複製程式碼

之所以會寫出這樣的程式碼,很大一部分是受到我們平時寫ListViewBaseAdapter的影響,因為我們淺意識地認為,呼叫了notifyDataSetChanged()方法之後,ViewPager就會去呼叫getItem方法來獲取新的Fragment以替換舊的Fragment,就好像我們使用ListVIew的時候,它會去回撥BaseAdaptergetView方法來獲取新的View一樣,執行上面的Demo,會有發現以下幾個問題:

  • 第一個問題:呼叫notifyDataSetChanged()之後,ViewPager當前存在的頁面中的Fragment不會發生變化。
  • 第二個問題:對於重新新增的介面,不會回撥getItem來獲取新的Fragment

2.2 原因分析 - 問題一

我們首先分析問題一:呼叫notifyDataSetChanged()之後,ViewPager當前存在的頁面中的Fragment不會發生變化。

首先,我們確定分析的場景:啟動DemoActivity,按照之前的分析,現在會給ViewPager新增兩個頁面,分別是index=0index=1,接著我們呼叫PagerAdapter#notifyDataSetChanged(),最終會走到ViewPager#dataSetChanged方法,我們看一下里面做了什麼:

    void dataSetChanged() {
        //在我們的例子中,返回的是4
        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();
        }
    }
複製程式碼

要理解上面的這段程式碼,首先要明白mItems是什麼,以及mItemsItemInfo中各個成員變數的含義:

  • mItems中的每一個ItemInfo和存在與ViewPager中的介面一一關聯。
  • ItemInfo中的含義:
    static class ItemInfo {
        Object object; //通過PagerAdapter#instantiateItem所返回的Object.
        int position; //這個Item所處的位置,也就是上面我們所說的index.
        boolean scrolling;
        float widthFactor;
        float offset;
    }
複製程式碼

此時,也就是我們位於index=0的頁面,mItems的內容為:

Fragment 知識梳理(3)   FragmentPagerAdapter 和 FragmentStatePagerAdapter 的數
而如果我們滑動到index=2的頁面,那麼mItems的內容變為:
Fragment 知識梳理(3)   FragmentPagerAdapter 和 FragmentStatePagerAdapter 的數

我們注意上面有一句關鍵的話:

final int newPos = mAdapter.getItemPosition(ii.object);
複製程式碼

對於它的返回值,有三種處理方式:

  • PagerAdapter.POSITION_UNCHANGED:這個ItemInfo在整個ViewPager的位置沒有發生改變。
  • PagerAdapter.POSITION_NONE:這個ItemInfo在整個ViewPager中已經不存在了。
  • ii.position != newPos,也就是說ItemInfoViewPager仍然需要存在,但是它的位置發生了改變。

也就是說,notifyDataSetChanged()只處理ViewPager當前已經存在的介面,而對於這些介面如何處理,則要依賴於getItemPosition的返回值,但是**FragmentPagerAdapter的返回值預設是POSITION_UNCHANGED**,因此,當前已經存在的介面不會發生任何改變。

2.3 原因分析 - 問題二

問題二:對於重新新增的介面,不會回撥getItem來獲取新的Fragment。 這個其實和notifyDataSetChanged()沒有關係,而是和FragmentPagerAdapter尋找Fragment的方式有關,它會優先從FragmentManager中尋找,找不到了才會回撥getItem來取新的Fragment。 但是我們在移除介面的是呼叫的是detach方法,因此FragmentManager中仍然儲存了Fragment的例項,在重新新增的時候就不會再回撥getItem來取了,這個我們在前一篇文章中已經分析過,就不貼具體的程式碼了。

三、FragmentStatePagerAdapter

我們把上面例子中的FragmentPagerAdapter替換成為FragmentStatePagerAdapter,採用一樣的更新方式,此時呼叫notifyDataSetChanged()之後,ViewPager當前存在的頁面中的Fragment依然不會發生變化,不重新整理的原因和FragmentPagerAdapter是相同的。 與FragmentPagerAdapter不同的是,對於重新新增的介面,會回撥getItem來獲取新的Fragment,這個原因在之前的文章中也分析過了。

四、實現一個高效的動態FragmentPagerAdapter

我們的需求和下面的這個介面類似:

Fragment 知識梳理(3)   FragmentPagerAdapter 和 FragmentStatePagerAdapter 的數
需求包括:

  • 支援動態地新增和移除介面,介面的個數和頻道的個數相同,並且是可變的。
  • 當頻道發生變化時,介面也要根據頻道的順序進行相應的改變,但是,如果上次存在的頻道,在編輯之後仍然存在,那麼應當複用之前的介面。

因此,我們繼承於FramentStatePagerAdapter

public abstract class FixedPagerAdapter<T> extends FragmentStatePagerAdapter {

    private List<ItemObject> mCurrentItems = new ArrayList<>();

    public FixedPagerAdapter(FragmentManager fragmentManager) {
        super(fragmentManager);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        while (mCurrentItems.size() <= position) {
            mCurrentItems.add(null);
        }
        Fragment fragment = (Fragment) super.instantiateItem(container, position);
        ItemObject object = new ItemObject(fragment, getItemData(position));
        mCurrentItems.set(position, object);
        return object;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        mCurrentItems.set(position, null);
        super.destroyItem(container, position, ((ItemObject) object).fragment);
    }

    @Override
    public int getItemPosition(Object object) {
        ItemObject itemObject = (ItemObject) object;
        if (mCurrentItems.contains(itemObject)) {
            T oldData = itemObject.t;
            int oldPosition = mCurrentItems.indexOf(itemObject);
            T newData = getItemData(oldPosition);
            if (equals(oldData, newData)) {
                return POSITION_UNCHANGED;
            } else {
                int newPosition = getDataPosition(oldData);
                return newPosition >= 0 ? newPosition : POSITION_NONE;
            }
        }
        return POSITION_UNCHANGED;
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        super.setPrimaryItem(container, position, ((ItemObject) object).fragment);
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return super.isViewFromObject(view, ((ItemObject) object).fragment);
    }

    public abstract T getItemData(int position);

    public abstract int getDataPosition(T t);

    public abstract boolean equals(T oldD, T newD);

    public class ItemObject {

        public Fragment fragment;
        public T t;

        public ItemObject(Fragment fragment, T t) {
            this.fragment = fragment;
            this.t = t;
        }
    }

}
複製程式碼

這裡:

  • 我們通過一個mCurrentItems儲存了當前頁面中對應的Fragment和其所包含的資料。
  • 最重要的是我們重寫了getItemPosition方法,根據不同的情況返回位置,這裡需要子類提供三個方面的資訊:
  • 新資料在某個位置的資料。
  • 某個資料在新資料中的位置。
  • 判斷兩個資料是否相等的標準。

現在,我們的Adapter只需要重寫很少的程式碼,就能實現資料的更新:

public class DemoActivity extends AppCompatActivity {

    private static final int INCREASE = 4;
    private FixedPagerAdapter mFixedPagerAdapter;
    private List<String> mTitles;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_not_update);
        TextView updateTv = (TextView) findViewById(R.id.tv_update);
        updateTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                updateFragments();
            }
        });
        initFragments();
    }

    private void initFragments() {
        mTitles = new ArrayList<>();
        for (int i = 0; i < INCREASE; i++) {
            mTitles.add(String.valueOf(i));
        }
        ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
        mFixedPagerAdapter = new MyFixedPagerAdapter(getSupportFragmentManager(), mTitles);
        viewPager.setAdapter(mFixedPagerAdapter);
    }

    private void updateFragments() {
        mTitles.clear();
        mTitles.add("3");
        mTitles.add("2");
        mFixedPagerAdapter.notifyDataSetChanged();
    }

    private class MyFixedPagerAdapter extends FixedPagerAdapter<String> {

        private List<String> mTitles;

        public MyFixedPagerAdapter(FragmentManager fragmentManager, List<String> titles) {
            super(fragmentManager);
            mTitles = titles;
        }

        @Override
        public String getItemData(int position) {
            return mTitles.size() > position ? mTitles.get(position) : null;
        }

        @Override
        public int getDataPosition(String s) {
            return mTitles.indexOf(s);
        }

        @Override
        public boolean equals(String oldD, String newD) {
            return TextUtils.equals(oldD, newD);
        }

        @Override
        public Fragment getItem(int position) {
            return LogcatFragment.newInstance(mTitles.get(position));
        }

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

}
複製程式碼

相關文章