目錄介紹
- 01.PagerAdapter簡單介紹
- 02.PagerAdapter抽象方法
- 03.PagerAdapter原理介紹
- 04.PagerAdapter緩存和銷毀
- 05.自定義PagerAdapter
- 06.PagerAdapter兩個子類
- 07.三種Adapter的總結
00.ViewPager相關
- ViewPager懶加載: https://juejin.im/post/5d37bb8df265da1b8b2ba01a
- 這篇部落格是接着上一篇繼續分析和實踐優化的。
- 使用場景
- 輪播圖:ViewPager+自定義PagerAdapter
- fragment:TabLayout+ViewPager+FragmentPagerAdapter+Fragment
- 子類繼承PagerAdapter需要實作方法說明
- Object instantiateItem(ViewGroup container, int position)
- 一句話:要顯示的頁面或需要緩存的頁面,會調用這個方法進行布局的初始化。
- 這個方法是ViewPager需要加載某個頁面時調用,container就是ViewPager自己,position頁面索引;
- 我們需要實作的是添加一個view到container中,然後傳回一個跟這個view能夠關聯起來的對象,這個對象可以是view自身,也可以是其他對象(比如FragmentPagerAdapter傳回的就是一個Fragment),關鍵是在isViewFromObject能夠将view和這個object關聯起來
- void destroyItem(ViewGroup container, int position, Object object)
- 一句話:當ViewPager需要銷毀一個頁面時調用,我們需要将position對應的view從container中移除。
- 這時參數除了position就隻有object,其實就是上面instantiateItem方法傳回的對象,這時要通過object找到對應的View,然後将其移除掉,如果你的instantiateItem方法傳回的就是View,這裡就直接強轉成View移除即可:container.removeView((View) object);如果不是,一般會自己建立一個List緩存view清單,然後根據position從List中找到對應的view移除;(當然你也可以不移除,記憶體洩漏)。
- FragmentPagerAdapter的實作是:mCurTransaction.detach((Fragment)object),其實也就是将fragemnt的view從container中移除
- isViewFromObject(View view, Object object)
- 一句話:這個方法用于判斷是否由對象生成界面,官方建議直接傳回 return view == object;。
- 從名稱了解起來像是判斷view是否來自object,跟進一步解釋應該是上面instantiateItem方法中
- 向container中添加的view和方法傳回的對象兩者之間一對一的關系;因為在ViewPager内部有個方法叫infoForChild,
- 這個方法是通過view去找到對應頁面資訊緩存類ItemInfo(内部調用了isViewFromObject),如果找不到,說明這個view是個野孩子,ViewPager會認為不是Adapter提供的View,是以這個View不會顯示出來;
- 總結一下:isViewFromObject 方法是讓view和object(内部為ItemInfo)一一對應起來
- int getItemPosition(Object object)
- 改方法是判斷目前object對應的View是否需要更新,在調用notifyDataSetChanged時會間接觸發該方法,
- 如果傳回POSITION_UNCHANGED表示該頁面不需要更新,如果傳回POSITION_NONE則表示該頁面無效了,需要銷毀并觸發destroyItem方法(并且有可能調用instantiateItem重新初始化這個頁面)
- Object instantiateItem(ViewGroup container, int position)
02.PagerAdapter原理介紹
- ViewPager+PagerAdapter的合作關系:
- ViewPager來控制一頁界面構造和銷毀的時機,使用回調來通知PagerAdapter具體做什麼,PagerAdapter隻需要按照相應的步驟做。當然為了使用得更好、提供更多的功能,又建議了使用View的回收工作和管理工作,同時提供當資料改變時的界面重新整理工作。
- instantiateItem(ViewGroup, int):
- 構造指定位置的頁面。adapter負責在這個方法中添加view到容器中,即使是在finishUpdate(ViewGroup)才保證完成的。在FragmentPagerAdapter和FragmentStatePagerAdapter中,都是傳回一個構造的Fragment.
- destroyItem(ViewGroup, populate, Object):
- 移除指定位置的頁面。adapter負責從容器中移除view,即是最後實在finishUpdate(ViewGroup)保證完成的。在FragmentPagerAdapter和FragmentStatePagerAdapter中,分别使用FragmentTransition.detach(Fragment)和FragmentTransition.remove(Fragment)來邏輯上銷毀Fragment.
- finishUpdate(ViewGroup):
- 當頁面的顯示變化完成式調用。在這裡,你一定保證所有的頁面從容器中合理的添加或移除掉。
- setPrimaryItem(ViewGroup, int, Object):
- 被ViewPager調用來通知adapter此時那個item應該被認為是主要的頁面,這個頁面将在目前頁面展示給使用者。正是因為這個方法,才有在ViewPager中實作Fragment懶加載的機制。
- isViewFromObject(View, Object):
- 指定目前頁面View是否和指定的key對象相關聯(這個key對象是在instantiateItem(ViewGroup, int)方法傳回的)。這個方法需要PagerAdapter恰當的實作。即隻要比對好鍵值對即可。FragmentPagerAdapter和FragmentStatePagerAdapter的實作: return ((Fragment)object).getView() == view;.
- 雖然簡單或很少使用到的一些方法不想細究,不過還是一次性分析完為好,如getPageTitle(int), getPageWidth(int), getItemPosition(Object)等。
- getPageTitle(int): 傳回每頁的标題,多用于關聯indicator
- getPageWidth(int): 傳回指定的頁面相對于ViewPager寬度的比例,範圍(0.f-1.f]。預設值為1.f, 即占滿整個螢幕。如果是0.5f, 那麼在初始狀态下,預設會出現前兩個頁面,而primary首頁面是在ViewPager的起始位置(通常是螢幕左側),直到最後一個頁面在螢幕右側,如果總共5個頁面,傳回值為0.2f, 那麼将一次性出現所有的頁面.
- getItemPosition(Object):
- 用于資料重新整理時的頁面處理方式。傳回值包括三類:POSITION_UNCHANGED表示位置沒有變化,即在添加或移除一頁或多頁之後該位置的頁面保持不變,可以用于一個ViewPager中最後幾頁的添加或移除時,保持前幾頁仍然不變;POSITION_NONE,表示目前頁不再作為ViewPager的一頁資料,将被銷毀,可以用于無視View緩存的重新整理;根據傳過來的參數Object來判斷這個key所指定的新的位置
- 在ViewPager三種Adapter的子view建立和銷毀的方法添加相關的日志代碼,如下:
@Override public void destroyItem(ViewGroup container, int position, Object object) { Log.d("yc", "destroyItem:" + position); //...省略部分代碼 } @Override public Object instantiateItem(ViewGroup container, int position) { Log.d("yc", "instantiateItem:" + position); //...省略部分代碼 }
- 滑動ViewPager翻頁,觀察控制台的輸出,三種Adapter針對不同界面、不同滑動方向的翻頁情況列印如下:
- 從圖中我們可以看到,三種Adapter在相同的情況下,ViewPager的子頁面銷毀和建立時機是一樣。通常所聽到的都是FragmentPagerAdapter會緩存所有的Fragment子項,而上圖中我們看到的是在滑動的過程中它的destroyItem方法被調用了,而在滑動回來時相對應的子項Fragment也确實調用instantiateItem方法。這樣看來根本就沒有緩存……
- 但是仔細對比了一下三個Adapter建立視圖的過程,發現上面推論有所欠缺。
- 因為在使用Fragment作為子視圖時,我們是通過getItem方法傳回Fragment的,單純從這裡列印instantiateItem的調用不代表Fragment真的完全被重新建立了(重新建立代表需要重新add,即從頭走一遍生命周期,但是在這裡不能證明),也可以通過兩個FragmentAdapter中instantiateItem的實作證明(觀察getItem方法的調用條件),是以又在Fragment對應的兩種Adapter的getItem中添加相應的log代碼,如下:
@Override public Fragment getItem(int position) { Log.d("ccc", "getItem:" + position); return fragmentList.get(position); }
- 針對不同情況,控制台輸出結果如下:
- 通過上圖我們可以看到,FragmentPagerAdapter在最後向右邊劃回來時并沒有調用getItem方法(getItem是建立一個新的Fragment),這也就說明了他沒有重新建立Fragment,證明了它會緩存所有Fragment,那麼它到底在哪裡做了緩存呢?具體看FragmentPagerAdapter分析……
- 比如,引導頁使用ViewPager,這個時候動态管理的Adapter,可以每次都會建立新view,銷毀舊View。節省記憶體消耗性能。可以說下面這種用的最多……
/** * <pre> * @author yangchong * blog : https://github.com/yangchong211 * time : 2016/3/18 * desc : 動态管理的Adapter。概念參照{@link android.support.v4.app.FragmentPagerAdapter} * 每次都會建立新view,銷毀舊View。節省記憶體消耗性能 * revise: 比如使用場景是啟動引導頁 * </pre> */ public abstract class AbsDynamicPagerAdapter extends PagerAdapter { @Override public boolean isViewFromObject(@NonNull View arg0, @NonNull Object arg1) { return arg0==arg1; } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView((View) object); } @Override public int getItemPosition(@NonNull Object object) { return super.getItemPosition(object); } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { View itemView = getView(container,position); container.addView(itemView); return itemView; } /** * 建立view * @param container container * @param position 索引 * @return */ public abstract View getView(ViewGroup container, int position); }
- 比如,常見有無限輪播圖,可以自動輪播,大家應該用的特别多。這個時候可以優化自定義輪播圖的PagerAdapter,建立集合用來存儲view,再次用的時候先取集合,沒有就建立。而不是頻繁建立視圖。
/** * <pre> * @author yangchong * blog : https://github.com/yangchong211 * time : 2016/3/18 * desc : AbsLoopPagerAdapter * revise: 如果是自動輪播圖的話就用這一個 * </pre> */ public abstract class AbsLoopPagerAdapter extends PagerAdapter { private BannerView mViewPager; /** * 用來存放View的集合 */ private ArrayList<View> mViewList = new ArrayList<>(); /** * 重新整理全部 */ @Override public void notifyDataSetChanged() { mViewList.clear(); initPosition(); super.notifyDataSetChanged(); } /** * 擷取item索引 * * POSITION_UNCHANGED表示位置沒有變化,即在添加或移除一頁或多頁之後該位置的頁面保持不變, * 可以用于一個ViewPager中最後幾頁的添加或移除時,保持前幾頁仍然不變; * * POSITION_NONE,表示目前頁不再作為ViewPager的一頁資料,将被銷毀,可以用于無視View緩存的重新整理; * 根據傳過來的參數Object來判斷這個key所指定的新的位置 * @param object objcet * @return */ @Override public int getItemPosition(@NonNull Object object) { return POSITION_NONE; } /** * 注冊資料觀察者監聽 * @param observer observer */ @Override public void registerDataSetObserver(@NonNull DataSetObserver observer) { super.registerDataSetObserver(observer); initPosition(); } private void initPosition(){ if (getRealCount()>1){ if (mViewPager.getViewPager().getCurrentItem() == 0&&getRealCount()>0){ int half = Integer.MAX_VALUE/2; int start = half - half%getRealCount(); setCurrent(start); } } } /** * 設定位置,利用反射實作 * @param index 索引 */ @TargetApi(Build.VERSION_CODES.KITKAT) private void setCurrent(int index){ try { Field field = ViewPager.class.getDeclaredField("mCurItem"); field.setAccessible(true); field.set(mViewPager.getViewPager(),index); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); } } public AbsLoopPagerAdapter(BannerView viewPager){ this.mViewPager = viewPager; } @Override public boolean isViewFromObject(@NonNull View arg0, @NonNull Object arg1) { return arg0==arg1; } /** * 如果頁面不是目前顯示的頁面也不是要緩存的頁面,會調用這個方法,将頁面銷毀。 * @param container container * @param position 索引 * @param object object */ @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView((View) object); Log.d("PagerAdapter","銷毀的方法"); } /** * 要顯示的頁面或需要緩存的頁面,會調用這個方法進行布局的初始化。 * @param container container * @param position 索引 * @return */ @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { int realPosition = position%getRealCount(); View itemView = findViewByPosition(container,realPosition); container.addView(itemView); Log.d("PagerAdapter","建立的方法"); return itemView; } /** * 這個是避免重複建立,如果集合中有,則取集合中的 * @param container container * @param position 索引 * @return */ private View findViewByPosition(ViewGroup container, int position){ for (View view : mViewList) { if (((int)view.getTag()) == position&&view.getParent()==null){ return view; } } View view = getView(container,position); view.setTag(position); mViewList.add(view); return view; } @Deprecated @Override public final int getCount() { //設定最大輪播圖數量 ,如果是1那麼就是1,不輪播;如果大于1則設定一個最大值,可以輪播 //return getRealCount(); return getRealCount()<=1?getRealCount(): Integer.MAX_VALUE; } /** * 擷取輪播圖數量 * @return 數量 */ public abstract int getRealCount(); /** * 建立view * @param container viewGroup * @param position 索引 * @return */ public abstract View getView(ViewGroup container, int position); }
- 還有一種場景,靜态輪播圖,也就是不會自動輪播,但是手指可以滑動,并且滑動到第一張不能往左滑動,滑動到最後一張不能向右滑動。這種場景,view添加進去就不管了,View就常在呢!
/** * <pre> * @author yangchong * blog : https://github.com/yangchong211 * time : 2016/3/18 * desc : 靜态存儲的Adapter,概念參照{@link android.support.v4.app.FragmentStatePagerAdapter} * view添加進去就不管了,View長在,記憶體不再 * revise: 如果是靜态輪播圖就用這個 * </pre> */ public abstract class AbsStaticPagerAdapter extends PagerAdapter { private ArrayList<View> mViewList = new ArrayList<>(); @Override public boolean isViewFromObject(@NonNull View arg0, @NonNull Object arg1) { return arg0==arg1; } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView((View) object); Log.d("PagerAdapter","銷毀的方法"); } @Override public void notifyDataSetChanged() { mViewList.clear(); super.notifyDataSetChanged(); } @Override public int getItemPosition(@NonNull Object object) { return POSITION_NONE; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { View itemView = findViewByPosition(container,position); container.addView(itemView); onBind(itemView,position); Log.d("PagerAdapter","建立的方法"); return itemView; } private View findViewByPosition(ViewGroup container, int position){ for (View view : mViewList) { if (((int)view.getTag()) == position&&view.getParent()==null){ return view; } } View view = getView(container,position); view.setTag(position); mViewList.add(view); return view; } public void onBind(View view, int position){} public abstract View getView(ViewGroup container, int position); }
- 這三種不同的使用場景,我們應該都見到過,那麼自定義adpater的時候能否再優化一下,ok,上面的方案剛好合适。如果有不同的想法,歡迎提出……該源代碼的開源位址: https://github.com/yangchong211/YCBanner
- PagerAdapter 的兩個直接子類 FragmentPagerAdapter 和 FragmentStatePagerAdapter 。而我們常常會在 ViewPager 和 Fragment 結合使用的時候來使用這兩個擴充卡。
6.1 FragmentPagerAdapter
- FragmentPagerAdapter 它将每一個頁面表示為一個 Fragment,并且每一個 Fragment 都将會儲存到 FragmentManager 當中。而且,當使用者沒可能再次回到頁面的時候,FragmentManager 才會将這個 Fragment 銷毀。
- FragmentPagerAdapter:對于不再需要的 fragment,選擇調用 onDetach() 方法,僅銷毀視圖,并不會銷毀 fragment 執行個體。
- 使用 FragmentPagerAdapter 需要實作兩個方法:
- public Fragment getItem(int position) 傳回的是對應的 Fragment 執行個體,一般我們在使用時,會通過構造傳入一個要顯示的 Fragment 的集合,我們隻要在這裡把對應的 Fragment 傳回就行了。
- public int getCount() 這個上面介紹過了傳回的是頁面的個數,我們隻要傳回傳入集合的長度就行了。
- 使用起來是非常簡單的,FragmentStatePagerAdapter 的使用也和上面一樣,那兩者到底有什麼差別呢?
- 錯誤說法
- 超出範圍的Fragment會被銷毀。是以之前,我一直認為的是,FragmentPagerAdapter中通常最多會保留3個Fragment, 超出左右兩側的Fragment将被銷毀,滑動到時又會被重新構造。
- PagerAdapter的實作類,使用将一直保留在FragmentManager中的Fragment來代表每一頁,直到使用者傳回上一頁。
- 當用于典型地使用多靜态化的Fragment時,FragmentPagerAdapter無疑是最好使用的,例如一組tabs. 每個使用者通路過的頁面的Fragment都将會保留在記憶體中,即使它的視圖層在不可見時已經被銷毀。這可能導緻使用比較大數量的記憶體,因為Fragment執行個體持有任意數量的狀态。如果使用大資料的頁面,考慮使用FragmentStatePagerAdapter.
- 從上面可以看出,即使是超出可視範圍和緩存範圍之外的Fragment,它的視圖将會被銷毀,但是它的執行個體将會保留在記憶體中,是以每一頁的Fragment至始至終都隻需要構造一次而已。通常是在首頁中使用FragmentPagerAdapter, 但是超出範圍的Fragment的視圖會被銷毀,我們也可以在Fragment中緩存View來避免狀态的丢失,也可以使用另外的機制,如緩存View的狀态。
@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); }
- 從上面源碼可以得出結論
- 當被銷毀時,Fragment并沒有從FragmentTransition中移除,而是調用了FragmentTransition.detach(Fragment)方法,這樣銷毀了Fragment的視圖,但是沒有移除Fragment本身。
- detach:對應執行的是Fragment生命周期中onPause()-onDestroyView()的方法,此時并沒有執行onDestroy和onDetach方法。是以在恢複時隻需要attach方法即可(可以在FragmentPagerAdapter的instantiateItem方法中看到調用,對應源碼下面給出),attach方法對應的是執行Fragment生命周期中onCreateView()-onResume()。
6.2 FragmentStatePagerAdapter
- FragmentStatePagerAdapter:會銷毀不再需要的 fragment,當目前事務送出以後,會徹底的将 fragmeng 從目前 Activity 的FragmentManager 中移除,state 标明,銷毀時,會将其 onSaveInstanceState(Bundle outState) 中的 bundle 資訊儲存下來,當使用者切換回來,可以通過該 bundle 恢複生成新的 fragment,也就是說,你可以在 onSaveInstanceState(Bundle outState) 方法中儲存一些資料,在 onCreate 中進行恢複建立。
- 使用 FragmentStatePagerAdapter 更省記憶體,但是銷毀後建立也是需要時間的。一般情況下,如果你是制作首頁面,就 3、4 個 Tab,那麼可以選擇使用 FragmentPagerAdapter,如果你是用于 ViewPager 展示數量特别多的條目時,那麼建議使用 FragmentStatePagerAdapter。
- PagerAdapter的實作類,使用Fragment來管理每一頁。這個類也會管理儲存和恢複Fragment的狀态。
- 當使用一個大數量頁面時,FragmentStatePagerAdapter将更加有用,工作機制類似于ListView. 當每頁不再可見時,整個Fragment将會被銷毀,隻保留Fragment的狀态。相對于FragmentPagerAdapter, 這個将允許頁面持有更少的記憶體。
@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); }
- 從源碼可以看出,當銷毀Fragment時,緩存了Fragment的狀态,并移除了Fragment的引用。而在構造時,顯示判斷是否已經在構造,如果是則直接傳回該Fragment, 如果不是,則重新構造一個新的Fragment, 并且如果已經緩存了狀态,則将改狀态傳入Fragment用于恢複狀态。
- 三種Adapter的緩存政策
- PagerAdapter:緩存三個,通過重寫instantiateItem和destroyItem達到建立和銷毀view的目的。
- FragmentPagerAdapter:内部通過FragmentManager來持久化每一個Fragment,在destroyItem方法調用時隻是detach對應的Fragment,并沒有真正移除!
- FragmentPagerStateAdapter:内部通過FragmentManager來管理每一個Fragment,在destroyItem方法,調用時移除對應的Fragment。
- 三個Adapter使用場景分析
- PagerAdapter:當所要展示的視圖比較簡單時适用
- FragmentPagerAdapter:當所要展示的視圖是Fragment,并且數量比較少時适用
- FragmentStatePagerAdapter:當所要展示的視圖是Fragment,并且數量比較多時适用
其他介紹
01.關于部落格彙總連結
02.關于我的部落格
- github: https://github.com/yangchong211
- 知乎: https://www.zhihu.com/people/yczbj/activities
- 簡書: http://www.jianshu.com/u/b7b2c6ed9284
- csdn: http://my.csdn.net/m0_37700275
- 喜馬拉雅聽書: http://www.ximalaya.com/zhubo/71989305/
- 開源中國: https://my.oschina.net/zbj1618/blog
- 泡在網上的日子: http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 郵箱:[email protected]
- 阿裡雲部落格: https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault頭條: https://segmentfault.com/u/xiangjianyu/articles
- 掘金: https://juejin.im/user/5939433efe88c2006afa0c6e