Fragment 知識梳理(4) FragmentPagerAdapter 和 FragmentStatePagerAdapter 解析

1、概述

在平時的開發當中,用到ViewPager的場景主要是如下兩種:緩存

  • 對於主頁中的每一個子頁面,用Fragment包裹起來,而後經過ViewPager來實現頁面之間的切換。
  • 廣告輪播圖。

其中對於第一種狀況,咱們經常會使用到兩個PagerAdapter的實現類,也就是FragmentStatePagerAdapterFragmentPagerAdapter,今天,咱們就來學習一下它們的使用方法,並進行對比。bash

2、FragmentPagerAdapter

2.1 使用

在咱們的例子中,咱們定義了一個Acitivity,它的佈局中包含有一個ViewPager。初始時候咱們會給mFragments列表中新建4Fragment實例,而後把它傳給繼承於FragmentPagerAdapter的適配器,LogcatFragment就是用來打印Fragment的生命週期:ide

private void initFPAFragments() {
        mFragments = new ArrayList<>();
        for (int i = 0; i < INCREASE; i++) {
            //初始時刻有4個Fragment,每一個Fragment和一條數據相關聯.
            mFragments.add(LogcatFragment.newInstance("index=" + i));
        }
        ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
        mFPAdapter = new FPAdapter(getSupportFragmentManager(), mFragments);
        viewPager.setAdapter(mFPAdapter);
    }

    private class FPAdapter extends FragmentPagerAdapter {

        private List<Fragment> mFragments;

        public FPAdapter(FragmentManager fm, List<Fragment> fragments) {
            super(fm);
            mFragments = fragments;
        }

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

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

    }
複製代碼

2.2 現象

使用過ViewPager的同窗都知道,ViewPager有一個setOffscreenPageLimit,它表示對於當ViewPager處於IDLE狀態時,它的左右兩端最多會保留多少個頁面,對於超出這個範圍的頁面有可能會須要從PageAdapter中進行重建,這裏咱們設置的是1,下面咱們進行一系列的操做,並觀察此時各個頁面及其內部的Fragment的變化狀況:函數

  • 第一步:當咱們第一次啓動Activity的時候,默認會添加它的左右兩個界面,因爲咱們位於第一個(index=0),所以會添加它及其右邊的界面(index=0),此時這兩個頁面當中內部的Fragment的生命週期以下圖所示:
    從咱們常常看到的Fragment生命週期的圖來看,就是下面紅色的部分:
  • 第二步:下面,咱們滑動到index=1的界面,此時index=2的頁面會被添加,它內部的Fragment所走的生命週期和上面徹底相同,因爲index=1左右兩邊的界面個數都爲1,所以不會有頁面被移除。
  • 第三步:繼續往右滑動到index=2的界面,此時會添加index=3的頁面,並移除index=0的頁面,其內部包含的Fragment的生命週期打印爲:
    能夠看到對於添加的index=3的頁面而言,它內部的Fragment所走的生命週期和index=0/1/2相同,而被移除的index=0的頁面內部的Fragment所走的生命週期爲:
  • 第四步:向右滑動到index=1的界面,此時index=0的界面須要被從新添加,而index=3的界面則須要被移除,此時的打印爲:
    這時候,對於從新添加的頁面index=0,它和第一次添加的時候有兩點不一樣:
  • 沒有再去自定義的FragmentPagerAdapter中取Fragment
  • 其內部的Fragment所走的生命週期不一樣,此時爲:

最後,咱們總結一下,對於三種狀況的頁面內部的Fragment所走生命週期的區別以下圖所示:佈局

  • 第一次添加的頁面
  • 從新添加的頁面
  • 移除的頁面

2.3 源碼解析

如今,咱們就開始解釋一下,爲何第一次添加從新添加的頁面內部對應的Fragment會有所不一樣,咱們只須要關注FragmentPagerAdapter內的兩個函數:學習

  • public Object instantiateItem(ViewGroup container, int position),添加頁面時回調。
  • public void destroyItem(ViewGroup container, int position, Object object),移除頁面時回調。
@Override
    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        //這裏的itemId返回的是對應position的頁面的惟一標識符.
        final long itemId = getItemId(position);
        //1.先是經過FragmentManager來找.
        String name = makeFragmentName(container.getId(), itemId);
        Fragment fragment = mFragmentManager.findFragmentByTag(name);
        //2.若是找到了,那麼調用attach方法.
        if (fragment != null) {
            if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
            mCurTransaction.attach(fragment);
        } else {
            //3.若是沒找到,那麼經過子類實現的getItem方法來獲取.
            fragment = getItem(position);
            //這裏調用的是add方法.
            mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
        }
        //根據須要,回調下面這兩個方法.
        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }
        //返回給ViewPager.
        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        //4.移除界面時,調用的是detach方法.
        mCurTransaction.detach((Fragment)object);
    }
複製代碼

注意看上面的註釋,就能解釋上面咱們看到的現象了:ui

  • 第一次添加界面的時候,因爲FragmentManager中沒有這個Fragment,所以須要經過自定義的FragmentPagerAdapter獲取,而後調用add方法,也就是上面代碼中的第**(3)**步,所走的生命週期爲onAttach() -> onResume()
  • 移除界面時,使用的是detach方法,也就是上面代碼中的第**(4)**步,接觸過Fragment的人都知道,這時候僅僅是Fragment的界面被從View樹上移除了而已,它的實例仍然被保存在FragmentManager當中,所走的生命週期爲onPause() -> onDestroyView()
  • 從新添加界面時,因爲此時去FragmentManager中能找到那個Fragment,因此調用的是attach方法,也就是上面代碼中的第**(2)**步,所走的生命週期爲onCreateView() -> onResume(),而且不須要再從自定義的FragmentPagerAdapter中獲取Fragment

整個邏輯以下圖所示: spa

3、FragmentStatePagerAdapter

3.1 現象

咱們的代碼基本不用改動,只須要把原來繼承於FragmentPagerAdapter的子類替換爲繼承FragmentStatePagerAdapter就能夠了。在第二章當中,咱們分析得很詳細,相信你們對於整個分析的套路已經理解,所以,爲了減小篇幅,咱們直接說結論,當進行和上面相同的操做以後,把頁面分爲三種類型:3d

  • 第一次添加
  • 從新添加
  • 移除

此時,它們內部的Fragment所走的生命週期爲: code

對於從新添加移除的界面,其內部的Fragment所走的生命週期都和FragmentPagerAdapter不一樣,下面,咱們就從源碼的角度,來看一下致使這些區別的緣由。

3.2 源碼解析

和前面相似,咱們只關注添加和移除時調用的那兩個方法:

@Override
    public Object instantiateItem(ViewGroup container, int position) {
        //若是mFragments中存在對應位置的fragment,那麼直接返回.
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        Fragment fragment = getItem(position);
        //多了恢復狀態的操做.
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
       //保證mFragments的大小和ViewPager往右滑動的最遠的index相同.
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        //設置對應位置.
        mFragments.set(position, fragment);
        //這裏很關鍵,調用的add方法.
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, fragment.isAdded() ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        mFragments.set(position, null);
        //調用的是remove方法.
        mCurTransaction.remove(fragment);
    }
複製代碼

從上面的源碼當中,總結出如下幾點:

  • FragmentStatePagerAdapter移除頁面的時候,調用的是remove方法,也就是說,FragmentManager中再也不有這個Fragment的實例,所走的生命週期爲onPause() -> onDetach()
  • 不管是添加頁面仍是從新添加頁面,它是經過add方法,而且每次都會經過自定義的FragmentStatePagerAdapter子類的getItem方法來獲取Fragment,因此它們內部的Fragment所走生命週期相同,都是從onAttach() -> onResume()
  • 對於ViewPager當前界面中所對應的Fragment,是經過一個mFragments列表來管理的,因爲此時沒有FragmentManager來幫咱們實現Fragment集合的狀態的保存和恢復,因此就須要咱們本身實現onSave/onRestore方法來進行狀態的保存和恢復。

整個流程以下圖所示:

4、總結

FragmentPagerAdapterFragmentStatePagerAdapter最大的區別就在於前者會把全部Fragment的示例都緩存在內存當中,然後者僅僅保存了ViewPager當前存在的頁面所對應的Fragment,當頁面被移除以後,這個Fragment的示例它也就再也不保存了。 固然,在咱們前面的例子中,雖然使用了FragmentStatePagerAdapter,可是因爲咱們在DemoActivity中用一個列表保存了全部的Fragment實例,所以它沒有被回收,若是但願讓頁面被移除的時候,其對應的Fragment實例也被回收,那麼咱們的FragmentStatePagerAdapter的子類應該寫成這樣:

private void initFSPAFragments() {
        ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
        mFSPAdapter = new FSPAdapter(getSupportFragmentManager());
        viewPager.setAdapter(mFSPAdapter);
    }

    private class FSPAdapter extends FragmentStatePagerAdapter {

        public FSPAdapter(FragmentManager fm) {
            super(fm);
        }

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

        @Override
        public int getCount() {
            return INCREASE;
        }
    }
複製代碼
相關文章
相關標籤/搜索