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

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

Deprecated

時間:2019年8月23日

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

前言

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

翻譯

/**
 * 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ù)時候會使用這個類的特定實現(xiàn)夜焦,如FragmentPagerAdapterFragmentStatePagerAdapter.

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

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

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

ViewPager使用一個鍵對象來關(guān)聯(lián)每一頁锉屈,而不是管理View。這個鍵用于追蹤和唯一標(biāo)識在adapter中獨立位置中的一頁垮耳。調(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)一次更新(添加和/或移除)完成之后將會調(diào)用finishUpdate(ViewGroup)來通知adapter, 提交關(guān)聯(lián)和/或取消關(guān)聯(lián)的操作。這三個方法就是用于ViewPager使用回調(diào)的方式來通知PagerAdapter來管理其中的頁面铃彰。

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

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

原理詳解

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

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

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

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

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

isViewFromObject(View, Object): 指定當(dāng)前頁面View是否和指定的key對象相關(guān)聯(lián)(這個key對象是在instantiateItem(ViewGroup, int)方法返回的)。這個方法需要PagerAdapter恰當(dāng)?shù)膶崿F(xiàn)骆膝。即只要匹配好鍵值對即可祭衩。FragmentPagerAdapterFragmentStatePagerAdapter的實現(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): 返回指定的頁面相對于ViewPager寬度的比例政钟,范圍(0.f-1.f]路克。默認(rèn)值為1.f, 即占滿整個屏幕。如果是0.5f, 那么在初始狀態(tài)下养交,默認(rèn)會出現(xiàn)前兩個頁面精算,而primary主頁面是在ViewPager的起始位置(通常是屏幕左側(cè)),直到最后一個頁面在屏幕右側(cè)碎连,如果總共5個頁面灰羽,返回值為0.2f, 那么將一次性出現(xiàn)所有的頁面.

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

// ...
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的對于Fragment的管理機(jī)制驳概。當(dāng)然我們也可以使用這個來優(yōu)化我們自己的PagerAdapter, 如主頁Banner.

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

FragmentPagerAdapter

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

/**
 * 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的實現(xiàn)類上枕,使用將一直保留在FragmentManager中的Fragment來代表每一頁,直到用戶返回上一頁弱恒。

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

從上面可以看出,即使是超出可視范圍和緩存范圍之外的Fragment并扇,它的視圖將會被銷毀去团,但是它的實例將會保留在內(nèi)存中抡诞,所以每一頁的Fragment至始至終都只需要構(gòu)造一次而已穷蛹。通常是在主頁中使用FragmentPagerAdapter, 但是超出范圍的Fragment的視圖會被銷毀,我們也可以在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)被銷毀時蛙吏,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的實現(xiàn)類谓着,使用Fragment來管理每一頁泼诱。這個類也會管理保存和恢復(fù)Fragment的狀態(tài)。

當(dāng)使用一個大數(shù)量頁面時赊锚,FragmentStatePagerAdapter將更加有用治筒,工作機(jī)制類似于ListView. 當(dāng)每頁不再可見時屉栓,整個Fragment將會被銷毀,只保留Fragment的狀態(tài)耸袜。相對于FragmentPagerAdapter, 這個將允許頁面持有更少的內(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時堤框,緩存了Fragment的狀態(tài)域滥,并移除了Fragment的引用。而在構(gòu)造時蜈抓,顯示判斷是否已經(jīng)在構(gòu)造骗绕,如果是則直接返回該Fragment, 如果不是,則重新構(gòu)造一個新的Fragment, 并且如果已經(jīng)緩存了狀態(tài)资昧,則將改狀態(tài)傳入Fragment用于恢復(fù)狀態(tài)酬土。

Fragment懶加載的原理

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

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

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

是否提供滑動

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

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

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

懶加載需要處理的幾個問題

  • 預(yù)加載

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

  • 視圖保存

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

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

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

懶加載實現(xiàn)

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

  • 重寫Fragment實現(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)視圖真正加載時調(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}只會走一遍生命周期
 * <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;
    }
}

而同時慎皱,仍然需要重寫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)視圖真正加載時調(diào)用
     */
    protected abstract void onRealViewLoaded(View view);
}

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

示例可以參考FragmentApp

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

不使用ViewPager就避免了預(yù)加載的問題茫多,同時也不會有Fragment是否用戶可見的問題,因為只有加載時忽刽,用戶才可見天揖。

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

使用示例可以參考FragmentApp

參考

FragmentNavigator

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市伞剑,隨后出現(xiàn)的幾起案子斑唬,更是在濱河造成了極大的恐慌,老刑警劉巖黎泣,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赖钞,死亡現(xiàn)場離奇詭異,居然都是意外死亡聘裁,警方通過查閱死者的電腦和手機(jī)雪营,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衡便,“玉大人献起,你說我怎么就攤上這事×蜕拢” “怎么了谴餐?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長呆抑。 經(jīng)常有香客問我岂嗓,道長,這世上最難降的妖魔是什么鹊碍? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任厌殉,我火速辦了婚禮,結(jié)果婚禮上侈咕,老公的妹妹穿的比我還像新娘公罕。我一直安慰自己,他們只是感情好耀销,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布楼眷。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪罐柳。 梳的紋絲不亂的頭發(fā)上掌腰,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音张吉,去河邊找鬼辅斟。 笑死,一個胖子當(dāng)著我的面吹牛芦拿,可吹牛的內(nèi)容都是我干的士飒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蔗崎,長吁一口氣:“原來是場噩夢啊……” “哼酵幕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起缓苛,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤芳撒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后未桥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笔刹,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年冬耿,在試婚紗的時候發(fā)現(xiàn)自己被綠了舌菜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡亦镶,死狀恐怖日月,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缤骨,我是刑警寧澤爱咬,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站绊起,受9級特大地震影響精拟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜虱歪,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一蜂绎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧实蔽,春花似錦荡碾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至铐尚,卻和暖如春拨脉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背宣增。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工玫膀, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人爹脾。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓帖旨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親灵妨。 傳聞我的和親對象是個殘疾皇子解阅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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