PagerAdapter分析與Fragment懶加載的幾種實(shí)現(xiàn)

PagerAdapter分析與Fragment懶加載的幾種實(shí)現(xiàn)

Deprecated

時(shí)間:2019年8月23日

新版的ViewPager的實(shí)現(xiàn)和之前不同,FragmentTransaction增加setMaxLifecycle(Fragment, Lifecycle.State)相關(guān)API,更好使用。下面實(shí)現(xiàn)方法個(gè)人不再推薦使用。

前言

相信使用過ViewPager的人都知道它的常規(guī)使用方法,當(dāng)然用得最多的仍然是FragmentPagerAdapterFragmentStatePagerFragment,但是會(huì)用不一定用得好,從剛開始開發(fā)APP到現(xiàn)在,我也用過無數(shù)遍,但是最近為了優(yōu)化界面,查看源碼才發(fā)現(xiàn)之前自己“不會(huì)用”。

翻譯

/**
 * Base class providing the adapter to populate pages inside of
 * a {@link ViewPager}.  You will most likely want to use a more
 * specific implementation of this, such as
 * {@link android.support.v4.app.FragmentPagerAdapter} or
 * {@link android.support.v4.app.FragmentStatePagerAdapter}.
 *
 * <p>When you implement a PagerAdapter, you must override the following methods
 * at minimum:</p>
 * <ul>
 * <li>{@link #instantiateItem(ViewGroup, int)}</li>
 * <li>{@link #destroyItem(ViewGroup, int, Object)}</li>
 * <li>{@link #getCount()}</li>
 * <li>{@link #isViewFromObject(View, Object)}</li>
 * </ul>
 *
 * <p>PagerAdapter is more general than the adapters used for
 * {@link android.widget.AdapterView AdapterViews}. Instead of providing a
 * View recycling mechanism directly ViewPager uses callbacks to indicate the
 * steps taken during an update. A PagerAdapter may implement a form of View
 * recycling if desired or use a more sophisticated method of managing page
 * Views such as Fragment transactions where each page is represented by its
 * own Fragment.</p>
 *
 * <p>ViewPager associates each page with a key Object instead of working with
 * Views directly. This key is used to track and uniquely identify a given page
 * independent of its position in the adapter. A call to the PagerAdapter method
 * {@link #startUpdate(ViewGroup)} indicates that the contents of the ViewPager
 * are about to change. One or more calls to {@link #instantiateItem(ViewGroup, int)}
 * and/or {@link #destroyItem(ViewGroup, int, Object)} will follow, and the end
 * of an update will be signaled by a call to {@link #finishUpdate(ViewGroup)}.
 * By the time {@link #finishUpdate(ViewGroup) finishUpdate} returns the views
 * associated with the key objects returned by
 * {@link #instantiateItem(ViewGroup, int) instantiateItem} should be added to
 * the parent ViewGroup passed to these methods and the views associated with
 * the keys passed to {@link #destroyItem(ViewGroup, int, Object) destroyItem}
 * should be removed. The method {@link #isViewFromObject(View, Object)} identifies
 * whether a page View is associated with a given key object.</p>
 *
 * <p>A very simple PagerAdapter may choose to use the page Views themselves
 * as key objects, returning them from {@link #instantiateItem(ViewGroup, int)}
 * after creation and adding them to the parent ViewGroup. A matching
 * {@link #destroyItem(ViewGroup, int, Object)} implementation would remove the
 * View from the parent ViewGroup and {@link #isViewFromObject(View, Object)}
 * could be implemented as <code>return view == object;</code>.</p>
 *
 * <p>PagerAdapter supports data set changes. Data set changes must occur on the
 * main thread and must end with a call to {@link #notifyDataSetChanged()} similar
 * to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data
 * set change may involve pages being added, removed, or changing position. The
 * ViewPager will keep the current page active provided the adapter implements
 * the method {@link #getItemPosition(Object)}.</p>
 */

翻譯:
ViewPager填充頁面需要提供的adapter的基類。大多數(shù)時(shí)候會(huì)使用這個(gè)類的特定實(shí)現(xiàn),如FragmentPagerAdapterFragmentStatePagerAdapter.

當(dāng)你實(shí)現(xiàn)一個(gè)PagerAdapter時(shí),你一定至少得重寫以下方法:

  • instantiateItem(ViewGroup, int)
  • destroyItem(ViewGroup, int, Object)
  • getCount()
  • isViewFromObject(View, Object)

相對(duì)使用AdapterView的adapter, 使用PagerAdapter更為簡單。ViewPager使用回調(diào)來指定操作的步驟,而不是直接使用循環(huán)回收機(jī)制。如果期望實(shí)現(xiàn)回收View,那么通過PagerAdapter也是可以實(shí)現(xiàn)的,或者使用更加復(fù)雜的方法來組織每一頁的View, 例如Fragment事務(wù)那樣,使用一個(gè)Fragment來管理View.

ViewPager使用一個(gè)鍵對(duì)象來關(guān)聯(lián)每一頁,而不是管理View。這個(gè)鍵用于追蹤和唯一標(biāo)識(shí)在adapter中獨(dú)立位置中的一頁。調(diào)用方法startUpdate(ViewGroup)表明ViewPager中的內(nèi)容需要更改。

通過調(diào)用一次或多次調(diào)用instantiateItem(ViewGroup, int)來構(gòu)造頁面視圖。
調(diào)用destroyItem(ViewGroup, int, Object)來取消ViewPager關(guān)聯(lián)的頁面視圖。
最后,當(dāng)一次更新(添加和/或移除)完成之后將會(huì)調(diào)用finishUpdate(ViewGroup)來通知adapter, 提交關(guān)聯(lián)和/或取消關(guān)聯(lián)的操作。這三個(gè)方法就是用于ViewPager使用回調(diào)的方式來通知PagerAdapter來管理其中的頁面。

一個(gè)非常簡單的方式就是使用每頁視圖作為key來關(guān)聯(lián)它們自己,在方法instantiateItem(ViewGroup, int)中創(chuàng)建和添加它們到ViewGroup之后,返回該頁視圖。與之相匹配的方法destroyItem(ViewGroup, int, Object)實(shí)現(xiàn)從ViewGroup中移除視圖。當(dāng)然必須在isViewFromObject(View, Object)中這樣實(shí)現(xiàn):return view == object;.

PagerAdapter支持?jǐn)?shù)據(jù)改變時(shí)刷新界面,數(shù)據(jù)改變必須在主線程中調(diào)用,并在數(shù)據(jù)改變完成后調(diào)用方法notifyDataSetChanged(), 和AdapterView中派生自BaseAdapter相似。一次數(shù)據(jù)的改變可能關(guān)聯(lián)著頁面的添加、移除、或改變位置。ViewPager將根據(jù)adapter中實(shí)現(xiàn)getItemPosition(Object)方法返回的結(jié)果,來判斷是否保留當(dāng)前已經(jīng)構(gòu)造的活動(dòng)頁面(即重用,而不完全自行構(gòu)造)。

原理詳解

ViewPager+PagerAdapter的合作關(guān)系:ViewPager來控制一頁界面構(gòu)造和銷毀的時(shí)機(jī),使用回調(diào)來通知PagerAdapter具體做什么,PagerAdapter只需要按照相應(yīng)的步驟做。當(dāng)然為了使用得更好、提供更多的功能,又建議了使用View的回收工作和管理工作,同時(shí)提供當(dāng)數(shù)據(jù)改變時(shí)的界面刷新工作。

instantiateItem(ViewGroup, int): 構(gòu)造指定位置的頁面。adapter負(fù)責(zé)在這個(gè)方法中添加view到容器中,即使是在finishUpdate(ViewGroup)才保證完成的。在FragmentPagerAdapterFragmentStatePagerAdapter中,都是返回一個(gè)構(gòu)造的Fragment.

destroyItem(ViewGroup, populate, Object): 移除指定位置的頁面。adapter負(fù)責(zé)從容器中移除view, 即是最后實(shí)在finishUpdate(ViewGroup)保證完成的。在FragmentPagerAdapterFragmentStatePagerAdapter中,分別使用FragmentTransition.detach(Fragment)FragmentTransition.remove(Fragment)來邏輯上銷毀Fragment.

finishUpdate(ViewGroup): 當(dāng)頁面的顯示變化完成式調(diào)用。在這里,你一定保證所有的頁面從容器中合理的添加或移除掉。

setPrimaryItem(ViewGroup, int, Object): 被ViewPager調(diào)用來通知adapter此時(shí)那個(gè)item應(yīng)該被認(rèn)為是主要的頁面,這個(gè)頁面將在當(dāng)前頁面展示給用戶。正是因?yàn)檫@個(gè)方法,才有在ViewPager中實(shí)現(xiàn)Fragment懶加載的機(jī)制。

isViewFromObject(View, Object): 指定當(dāng)前頁面View是否和指定的key對(duì)象相關(guān)聯(lián)(這個(gè)key對(duì)象是在instantiateItem(ViewGroup, int)方法返回的)。這個(gè)方法需要PagerAdapter恰當(dāng)?shù)膶?shí)現(xiàn)。即只要匹配好鍵值對(duì)即可。FragmentPagerAdapterFragmentStatePagerAdapter的實(shí)現(xiàn): return ((Fragment)object).getView() == view;.

雖然簡單或很少使用到的一些方法不想細(xì)究,不過還是一次性分析完為好,如getPageTitle(int), getPageWidth(int), getItemPosition(Object)等。

getPageTitle(int): 返回每頁的標(biāo)題,多用于關(guān)聯(lián)indicator

getPageWidth(int): 返回指定的頁面相對(duì)于ViewPager寬度的比例,范圍(0.f-1.f]。默認(rèn)值為1.f, 即占滿整個(gè)屏幕。如果是0.5f, 那么在初始狀態(tài)下,默認(rèn)會(huì)出現(xiàn)前兩個(gè)頁面,而primary主頁面是在ViewPager的起始位置(通常是屏幕左側(cè)),直到最后一個(gè)頁面在屏幕右側(cè),如果總共5個(gè)頁面,返回值為0.2f, 那么將一次性出現(xiàn)所有的頁面.

getItemPosition(Object): 用于數(shù)據(jù)刷新時(shí)的頁面處理方式。返回值包括三類:POSITION_UNCHANGED表示位置沒有變化,即在添加或移除一頁或多頁之后該位置的頁面保持不變,可以用于一個(gè)ViewPager中最后幾頁的添加或移除時(shí),保持前幾頁仍然不變;POSITION_NONE,表示當(dāng)前頁不再作為ViewPager的一頁數(shù)據(jù),將被銷毀,可以用于無視View緩存的刷新;根據(jù)傳過來的參數(shù)Object來判斷這個(gè)key所指定的新的位置,如總共有5個(gè)頁面,我們想交換第0個(gè)和第4個(gè)的頁面,那么在返回時(shí),可以參考下面的代碼。

// ...
private List<Object> mItems = new ArrayList<>();

{
    for (int i = 0; i < 5; i++) {
        mItems.add(null);
    }
}

@Override
public int getItemPosition(Object object) {
    int position = mItems.indexOf(object);
    if (position == 0) {
        return 4;
    } else if (position == 4) {
        return 0;
    } else {
        // 下面兩種都可以
//            return POSITION_UNCHANGED;
        return position;
    }
}
// ...

saveState(): 保存頁面狀態(tài)。用得不多,見到的也是在FragmentStatePagerAdapter中使用,有不同于FragmentPagerAdapter的對(duì)于Fragment的管理機(jī)制。當(dāng)然我們也可以使用這個(gè)來優(yōu)化我們自己的PagerAdapter, 如主頁Banner.

restoreState(Parcelable, ClassLoader): 和saveState()搭配使用,用于恢復(fù)頁面狀態(tài)。

FragmentPagerAdapter

可以從很多文章中看到類似這樣的一句話:超出范圍的Fragment會(huì)被銷毀。所以之前,我一直認(rèn)為的是,FragmentPagerAdapter中通常最多會(huì)保留3個(gè)Fragment, 超出左右兩側(cè)的Fragment將被銷毀,滑動(dòng)到時(shí)又會(huì)被重新構(gòu)造。不過這是錯(cuò)誤的。下面是翻譯。

/**
 * Implementation of {@link PagerAdapter} that
 * represents each page as a {@link Fragment} that is persistently
 * kept in the fragment manager as long as the user can return to the page.
 *
 * <p>This version of the pager is best for use when there are a handful of
 * typically more static fragments to be paged through, such as a set of tabs.
 * The fragment of each page the user visits will be kept in memory, though its
 * view hierarchy may be destroyed when not visible.  This can result in using
 * a significant amount of memory since fragment instances can hold on to an
 * arbitrary amount of state.  For larger sets of pages, consider
 * {@link FragmentStatePagerAdapter}.
 *
 * ...
 */

翻譯:

PagerAdapter的實(shí)現(xiàn)類,使用將一直保留在FragmentManager中的Fragment來代表每一頁,直到用戶返回上一頁。

當(dāng)用于典型地使用多靜態(tài)化的Fragment時(shí),FragmentPagerAdapter無疑是最好使用的,例如一組tabs. 每個(gè)用戶訪問過的頁面的Fragment都將會(huì)保留在內(nèi)存中,即使它的視圖層在不可見時(shí)已經(jīng)被銷毀。這可能導(dǎo)致使用比較大數(shù)量的內(nèi)存,因?yàn)?code>Fragment實(shí)例持有任意數(shù)量的狀態(tài)。如果使用大數(shù)據(jù)的頁面,考慮使用FragmentStatePagerAdapter.

從上面可以看出,即使是超出可視范圍和緩存范圍之外的Fragment,它的視圖將會(huì)被銷毀,但是它的實(shí)例將會(huì)保留在內(nèi)存中,所以每一頁的Fragment至始至終都只需要構(gòu)造一次而已。通常是在主頁中使用FragmentPagerAdapter, 但是超出范圍的Fragment的視圖會(huì)被銷毀,我們也可以在Fragment中緩存View來避免狀態(tài)的丟失,也可以使用另外的機(jī)制,如緩存View的狀態(tài)。

@Override
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);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        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;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
            + " v=" + ((Fragment)object).getView());
    mCurTransaction.detach((Fragment)object);
}

從上面源碼可以看出,當(dāng)被銷毀時(shí),Fragment并沒有從FragmentTransition中移除,而是調(diào)用了FragmentTransition.detach(Fragment)方法,這樣銷毀了Fragment的視圖,但是沒有移除Fragment本身。

FragmentStatePagerAdapter

/**
 * Implementation of {@link PagerAdapter} that
 * uses a {@link Fragment} to manage each page. This class also handles
 * saving and restoring of fragment's state.
 *
 * <p>This version of the pager is more useful when there are a large number
 * of pages, working more like a list view.  When pages are not visible to
 * the user, their entire fragment may be destroyed, only keeping the saved
 * state of that fragment.  This allows the pager to hold on to much less
 * memory associated with each visited page as compared to
 * {@link FragmentPagerAdapter} at the cost of potentially more overhead when
 * switching between pages.
 *
 * ...
 */

PagerAdapter的實(shí)現(xiàn)類,使用Fragment來管理每一頁。這個(gè)類也會(huì)管理保存和恢復(fù)Fragment的狀態(tài)。

當(dāng)使用一個(gè)大數(shù)量頁面時(shí),FragmentStatePagerAdapter將更加有用,工作機(jī)制類似于ListView. 當(dāng)每頁不再可見時(shí),整個(gè)Fragment將會(huì)被銷毀,只保留Fragment的狀態(tài)。相對(duì)于FragmentPagerAdapter, 這個(gè)將允許頁面持有更少的內(nèi)存。

@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 (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            return f;
        }
    }

    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 (mFragments.size() <= position) {
        mFragments.add(null);
    }
    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
    mFragments.set(position, fragment);
    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();
    }
    if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
            + " v=" + ((Fragment)object).getView());
    while (mSavedState.size() <= position) {
        mSavedState.add(null);
    }
    mSavedState.set(position, fragment.isAdded()
            ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
    mFragments.set(position, null);

    mCurTransaction.remove(fragment);
}

從源碼可以看出,當(dāng)銷毀Fragment時(shí),緩存了Fragment的狀態(tài),并移除了Fragment的引用。而在構(gòu)造時(shí),顯示判斷是否已經(jīng)在構(gòu)造,如果是則直接返回該Fragment, 如果不是,則重新構(gòu)造一個(gè)新的Fragment, 并且如果已經(jīng)緩存了狀態(tài),則將改狀態(tài)傳入Fragment用于恢復(fù)狀態(tài)。

Fragment懶加載的原理

概念:當(dāng)需要時(shí)才加載,加載之后一直保持該對(duì)象。

而關(guān)于Fragment實(shí)現(xiàn)的PagerAdapter都沒有完全保存其引用和狀態(tài)。FragmentPageAdapter需要重建視圖,FragmentStatePageAdapter使用狀態(tài)恢復(fù),View都被銷毀,但是恢復(fù)的方式不同,而通常我們想得到的結(jié)果是,Fragment一旦被加載,其視圖也不會(huì)被銷毀,即不會(huì)再重新走一遍生命周期。而且ViewPager為了實(shí)現(xiàn)滑動(dòng)效果,都是預(yù)加載左右兩側(cè)的頁面。

我們通常想要實(shí)現(xiàn)的兩種效果:不提供滑動(dòng),需要時(shí)才構(gòu)造,并且只走一遍生命周期,避免在Fragment中做過多的狀態(tài)保存和恢復(fù),參考斗魚、QQ、全名,這也是大多數(shù)APP的實(shí)現(xiàn)方式;提供滑動(dòng),但是如果沒有真正顯示在界面上,那么使用站位頁面代替真正的數(shù)據(jù)頁面,參考微信,使用這種實(shí)現(xiàn)的較少。

是否提供滑動(dòng)

  • 提供滑動(dòng),當(dāng)然使用ViewPager是最為方便的,但是會(huì)預(yù)加載新的頁面,所以不可避免得,會(huì)提前加載數(shù)據(jù),或是使用站位頁面的方式來實(shí)現(xiàn)懶加載。

  • 不提供滑動(dòng),一種方式是使用ViewPager, 但是限制其不可滑動(dòng),同時(shí)在選擇指定的頁面時(shí),直接跳轉(zhuǎn),而不是翻頁的方式;另外的實(shí)現(xiàn)方式則是完全拋棄ViewPager,自己使用FragmentManager來管理Fragment, 這種方式的缺點(diǎn)是,離開了ViewPager,那么與之對(duì)應(yīng)的indicator庫也可能不能使用,所以會(huì)要求在這方面多做一些工作。

ViewPager中實(shí)現(xiàn)懶加載的原理

懶加載需要處理的幾個(gè)問題

  • 預(yù)加載

雖然沒有顯示在界面上,但是當(dāng)前頁面的上一頁和下一頁的Fragment已經(jīng)執(zhí)行了一個(gè)Fragment能夠顯示在界面上的所有生命周期方法,但是我們想在跳轉(zhuǎn)到該頁時(shí)才真正構(gòu)造數(shù)據(jù)視圖和請(qǐng)求數(shù)據(jù)。那么我們可以使用一個(gè)占位視圖,那么可以想到使用ViewStub,當(dāng)真正跳轉(zhuǎn)到該頁時(shí),執(zhí)行ViewStub.inflate()方法,加載真正的數(shù)據(jù)視圖和請(qǐng)求數(shù)據(jù)。

  • 視圖保存

當(dāng)某一頁超出可視范圍和預(yù)加載范圍,那么它將會(huì)被銷毀,FragmentStatePagerAdapter銷毀整個(gè)Fragment, 我們可以自己保存該Fragment, 或使用FragmentPagerAdapterFragmentTransition來保留Fragment的引用。雖然這樣,但是它的周期方法已經(jīng)走完,那么我們只能手動(dòng)的保存FragmentView的引用,當(dāng)再次重新進(jìn)入新的聲明周期方法時(shí),返回原來的View

  • 是否已經(jīng)被用戶所看到

其實(shí)本身而言,FragmentManager并沒有提供為Fragment被用戶所看到的回調(diào)方法,而是在FragmentPagerAdapterFragmentStatePagerAdapter中,調(diào)用了Fragment.setUserVisibleHint(boolean)來表明Fragment是否已經(jīng)被作為primaryFragment. 所以這個(gè)方法可以被認(rèn)為是一個(gè)回調(diào)方法。

懶加載實(shí)現(xiàn)

通常想到的,我們是在Fragment中來根據(jù)生命周期來控制視圖的緩存和數(shù)據(jù)的加載來實(shí)現(xiàn)懶加載的效果,但是我們也可以自定義PagerAdapter來實(shí)現(xiàn)懶加載。

  • 重寫Fragment實(shí)現(xiàn)懶加載
/**
 * Created by Mycroft on 2017/1/9.
 */
public abstract class LazyFragment extends Fragment {

    // Fragment的根View
    private View mRootView;

    // 檢測聲明周期中,是否已經(jīng)構(gòu)建視圖
    private boolean mViewCreated = false;

    // 占位圖
    private ViewStubCompat mViewStub;

    @Nullable
    @Override
    public final View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        if (mRootView != null) {
            mViewCreated = true;
            return mRootView;
        }

        final Context context = inflater.getContext();
        FrameLayout root = new FrameLayout(context);
        mViewStub = new ViewStubCompat(context, null);
        mViewStub.setLayoutResource(getResId());
        root.addView(mViewStub, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
        root.setLayoutParams(new ViewGroup.MarginLayoutParams(ViewGroup.MarginLayoutParams.MATCH_PARENT, ViewGroup.MarginLayoutParams.MATCH_PARENT));

        mRootView = root;

        mViewCreated = true;
        if (mUserVisible) {
            realLoad();
        }
        return mRootView;
    }

    private boolean mUserVisible = false;

    @Override
    public final void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        mUserVisible = isVisibleToUser;
        if (mUserVisible && mViewCreated) {
            realLoad();
        }
    }

    // 判斷是否已經(jīng)加載
    private boolean mLoaded = false;

    /**
     * 控制只允許加載一次
     */
    private void realLoad() {
        if (mLoaded) {
            return;
        }

        mLoaded = true;
        onRealViewLoaded(mViewStub.inflate());
    }

    @Override
    public void onDestroyView() {
        mViewCreated = false;
        super.onDestroyView();
    }

    /**
     * 獲取真正的數(shù)據(jù)視圖
     *
     * @return
     */
    protected abstract int getResId();

    /**
     * 當(dāng)視圖真正加載時(shí)調(diào)用
     */
    protected abstract void onRealViewLoaded(View view);
}

示例可以參考FragmentApp

  • 重寫PagerAdapter

重寫PagerAdapter解決的是Fragment生命周期所帶來的視圖保存的問題。

/**
 * 和{@link android.support.v4.app.FragmentPagerAdapter}唯一的不同是
 * 使用{@link FragmentTransaction#add(int, Fragment, String)}和{@link FragmentTransaction#remove(Fragment)}
 * 來代替{@link FragmentTransaction#attach(Fragment)}和{@link FragmentTransaction#detach(Fragment)}
 * <p>
 * 這樣{@link Fragment}只會(huì)走一遍生命周期
 * <p>
 * Created by Mycroft on 2017/1/9.
 */
public abstract class LazyFragmentPagerAdapter extends PagerAdapter {
    private static final String TAG = "LazyFragmentPagerAdapter";
    private static final boolean DEBUG = true;

    private final FragmentManager mFragmentManager;
    private FragmentTransaction mCurTransaction = null;
    private Fragment mCurrentPrimaryItem = null;

    public LazyFragmentPagerAdapter(FragmentManager fm) {
        mFragmentManager = fm;
    }

    /**
     * Return the Fragment associated with a specified position.
     */
    public abstract Fragment getItem(int position);

    @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 (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);
        if (fragment != null) {
            if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
            mCurTransaction.show(fragment);
        } else {
            fragment = getItem(position);
            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;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
                + " v=" + ((Fragment) object).getView());
        mCurTransaction.hide((Fragment) object);
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;
        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) {
        return ((Fragment) object).getView() == view;
    }

    @Override
    public Parcelable saveState() {
        return null;
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) {
    }

    /**
     * Return a unique identifier for the item at the given position.
     * <p>
     * <p>The default implementation returns the given position.
     * Subclasses should override this method if the positions of items can change.</p>
     *
     * @param position Position within this adapter
     * @return Unique identifier for the item at position
     */
    public long getItemId(int position) {
        return position;
    }

    private static String makeFragmentName(int viewId, long id) {
        return "android:switcher:" + viewId + ":" + id;
    }
}

而同時(shí),仍然需要重寫Fragment來進(jìn)行預(yù)加載

/**
 * Created by Mycroft on 2017/1/9.
 */
public abstract class BaseLazyFragment extends Fragment {

    // 檢測聲明周期中,是否已經(jīng)構(gòu)建視圖
    private boolean mViewCreated = false;

    // 占位圖
    private ViewStubCompat mViewStub;

    @Nullable
    @Override
    public final View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

        final Context context = inflater.getContext();
        FrameLayout root = new FrameLayout(context);
        mViewStub = new ViewStubCompat(context, null);
        mViewStub.setLayoutResource(getResId());
        root.addView(mViewStub, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
        root.setLayoutParams(new ViewGroup.MarginLayoutParams(ViewGroup.MarginLayoutParams.MATCH_PARENT, ViewGroup.MarginLayoutParams.MATCH_PARENT));

        mViewCreated = true;
        if (mUserVisible) {
            realLoad();
        }
        return root;
    }

    private boolean mUserVisible = false;

    @Override
    public final void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        mUserVisible = isVisibleToUser;
        if (mUserVisible && mViewCreated) {
            realLoad();
        }
    }

    // 判斷是否已經(jīng)加載
    private boolean mLoaded = false;

    /**
     * 控制只允許加載一次
     */
    private void realLoad() {
        if (mLoaded) {
            return;
        }

        mLoaded = true;
        onRealViewLoaded(mViewStub.inflate());
    }

    @Override
    public void onDestroyView() {
        mViewCreated = false;
        super.onDestroyView();
    }

    /**
     * 獲取真正的數(shù)據(jù)視圖
     *
     * @return
     */
    protected abstract int getResId();

    /**
     * 當(dāng)視圖真正加載時(shí)調(diào)用
     */
    protected abstract void onRealViewLoaded(View view);
}

LazyFragment唯一的不同是不用自己來保留根View.

示例可以參考FragmentApp

不使用ViewPager,實(shí)現(xiàn)懶加載的原理

不使用ViewPager就避免了預(yù)加載的問題,同時(shí)也不會(huì)有Fragment是否用戶可見的問題,因?yàn)橹挥屑虞d時(shí),用戶才可見。

關(guān)于這一點(diǎn),已經(jīng)有了開源項(xiàng)目,可以參考FragmentNavigator

使用示例可以參考FragmentApp

參考

FragmentNavigator

如何高效的使用ViewPager,以及FragmentPagerAdapter與FragmentStatePagerAdapter的區(qū)別

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容