摘要:最后,當一次更新添加和或移除完成之后將會調用來通知提交關聯和或取消關聯的操作。懶加載的實現弊端概念當需要時才加載,加載之后一直保持該對象。而且為了實現滑動效果,都是預加載左右兩側的頁面。預加載的預加載機制。
目錄介紹
01.ViewPager簡單介紹
02.ViewPager弊端分析
03.ViewPager預加載
04.ViewPager部分源碼
05.懶加載出現問題
06.如何實現預加載機制
07.懶加載配合狀態管理器
好消息博客筆記大匯總【16年3月到至今】,包括Java基礎及深入知識點,Android技術博客,Python學習筆記等等,還包括平時開發中遇到的bug匯總,當然也在工作之余收集了大量的面試題,長期更新維護并且修正,持續完善……開源的文件是markdown格式的!同時也開源了生活博客,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請注明出處,謝謝!
鏈接地址:https://github.com/yangchong2...
如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起于忽微,量變引起質變!
01.ViewPager簡單介紹ViewPager使用一個鍵對象來關聯每一頁,而不是管理View。這個鍵用于追蹤和唯一標識在adapter中獨立位置中的一頁。調用方法startUpdate(ViewGroup)表明ViewPager中的內容需要更改。
通過調用一次或多次調用instantiateItem(ViewGroup, int)來構造頁面視圖。
調用destroyItem(ViewGroup, int, Object)來取消ViewPager關聯的頁面視圖。
最后,當一次更新(添加和/或移除)完成之后將會調用finishUpdate(ViewGroup)來通知adapter, 提交關聯和/或取消關聯的操作。這三個方法就是用于ViewPager使用回調的方式來通知PagerAdapter來管理其中的頁面。
一個非常簡單的方式就是使用每頁視圖作為key來關聯它們自己,在方法instantiateItem(ViewGroup, int)中創建和添加它們到ViewGroup之后,返回該頁視圖。與之相匹配的方法destroyItem(ViewGroup, int, Object)實現從ViewGroup中移除視圖。當然必須在isViewFromObject(View, Object)中這樣實現:return view == object;.
PagerAdapter支持數據改變時刷新界面,數據改變必須在主線程中調用,并在數據改變完成后調用方法notifyDataSetChanged(), 和AdapterView中派生自BaseAdapter相似。一次數據的改變可能關聯著頁面的添加、移除、或改變位置。ViewPager將根據adapter中實現getItemPosition(Object)方法返回的結果,來判斷是否保留當前已經構造的活動頁面(即重用,而不完全自行構造)。
02.ViewPager弊端分析普通的viewpager如果你不使用setoffscreenpagelimit(int limit)這個方法去設置默認加載數的話是會默認加載頁面的左右兩頁的,也就是說當你進入viewpager第一頁的時候第二頁和第一頁是會被一起加載的,這樣同時加載就會造成一些問題,試想我們如果設置了setoffscreenpagelimit為3的話,那么進入viewpager以后就會同時加載4個fragment,像我們平時的項目中在這些fragment中一般都是會發送網絡請求的,也就是說我們有4個fragment同時發送網絡請求去獲取數據,這樣的結果顯而易見給用戶的體驗是不好的(如:浪費用戶流量,造成卡頓等等)。
懶加載的實現弊端
概念:當需要時才加載,加載之后一直保持該對象。
而關于Fragment實現的PagerAdapter都沒有完全保存其引用和狀態。FragmentPageAdapter需要重建視圖,FragmentStatePageAdapter使用狀態恢復,View都被銷毀,但是恢復的方式不同,而通常我們想得到的結果是,Fragment一旦被加載,其視圖也不會被銷毀,即不會再重新走一遍生命周期。而且ViewPager為了實現滑動效果,都是預加載左右兩側的頁面。
我們通常想要實現的兩種效果:不提供滑動,需要時才構造,并且只走一遍生命周期,避免在Fragment中做過多的狀態保存和恢復。
03.ViewPager預加載
ViewPager的預加載機制。那么,我們可不可以設置ViewPager的預加載為0,不就解決問題了嗎?也就是代碼這樣操作:
vp.setOffscreenPageLimit(0);
然后看一下源碼
即使你設置為0,那么還是會在里面判斷后設為默認值1。所以這個方法是行不通的。
public void setOffscreenPageLimit(int limit) { if (limit < 1) { Log.w("ViewPager", "Requested offscreen page limit " + limit + " too small; defaulting to " + 1); limit = 1; } if (limit != this.mOffscreenPageLimit) { this.mOffscreenPageLimit = limit; this.populate(); } }
ViewPager默認情況下的加載,當切換到當前頁面時,會默認預加載左右兩側的布局到ViewPager中,盡管兩側的View并不可見的,我們稱這種情況叫預加載;由于ViewPager對offscreenPageLimit設置了限制,頁面的預加載是不可避免……
初始化緩存(mOffscreenPageLimit == 1)
當初始化時,當前顯示頁面是第0頁;mOffscreenPageLimit為1,所以預加載頁面為第1頁,再往后的頁面就不需要加載了(這里的2, 3, 4頁)
中間頁面緩存(mOffscreenPageLimit == 1)
當向右滑動到第2頁時,左右分別需要緩存一頁,第0頁就需要銷毀掉,第3頁需要預加載,第4頁不需要加載
04.ViewPager部分源碼
ViewPager.setAdapter方法
銷毀舊的Adapter數據,用新的Adaper更新UI
清除舊的Adapter,對已加載的item調用destroyItem,
將自身滾動到初始位置this.scrollTo(0, 0)
設置PagerObserver: mAdapter.setViewPagerObserver(mObserver);
調用populate()方法計算并初始化View(這個方法后面會詳細介紹)
如果設置了OnAdapterChangeListener,進行回調
ViewPager.populate(int newCurrentItem)
該方法是ViewPager非常重要的方法,主要根據參數newCurrentItem和mOffscreenPageLimit計算出需要初始化的頁面和需要銷毀頁面,然后通過調用Adapter的instantiateItem和destroyItem兩個方法初始化新頁面和銷毀不需要的頁面!
根據newCurrentItem和mOffscreenPageLimit計算要加載的page頁面,計算出startPos和endPos
根據startPos和endPos初始化頁面ItemInfo,先從緩存里面獲取,如果沒有就調用addNewItem方法,實際調用mAdapter.instantiateItem
將不需要的ItemInfo移除: mItems.remove(itemIndex),并調用mAdapter.destroyItem方法
設置LayoutParams參數(包括position和widthFactor),根據position排序待繪制的View列表:mDrawingOrderedChildren,重寫了getChildDrawingOrder方法
最后一步獲取當前顯示View的焦點:currView.requestFocus(View.FOCUS_FORWARD)
ViewPager.dataSetChanged()
當調用Adapter的notifyDataSetChanged時,會觸發這個方法,該方法會重新計算當前頁面的position,
移除需要銷毀的頁面的ItemInfo對象,然后再調用populate方法刷新頁面
循環mItems(每個page對應的ItemInfo對象),調用int newPos = mAdapter.getItemPosition方法
當newPos等于PagerAdapter.POSITION_UNCHANGED表示當前頁面不需要更新,不用銷毀,當newPos等于PagerAdapter.POSITION_NONE時,需要更新,移除item,調用mAdapter.destroyItem
循環完成后,最后計算出顯示頁面的newCurrItem,調用setCurrentItemInternal(newCurrItem, false, true)方法更新UI(實際調用populate方法重新計算頁面信息)
ViewPager.scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected)
滑動到指定頁面,內部會觸發OnPageChangeListener
ViewPager.calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo)
這個方法主要用于計算每個頁面對應ItemInfo的offset變量,這個變量用于記錄當前view在所有緩存View中(包含當前顯示頁)的索引,用于布局的時候計算該View應該放在哪個位置
在populate方法中更新完頁面數據后,會調用該方法計算所有頁面的offset
05.懶加載出現問題
發現Fragment中有一個setUserVisibleHint(boolean isVisibleToUser)方法,這個方法就是告訴用戶,UI對用戶是否可見,可以做懶加載初始化操作。
因為ViewPager會加載好多Fragment,為了節省內容等會在Fragment不可見的某個時候調用onDestroyView()將用戶界面銷毀掉但是Fragment的實例還在,所以可能第一次加載沒有問題,但是再次回到第一個Fragment再去加載的時候就會出現UI對用戶可見但是視圖還沒有初始化。
懶加載需要處理的幾個問題
預加載,雖然沒有顯示在界面上,但是當前頁面的上一頁和下一頁的Fragment已經執行了一個Fragment能夠顯示在界面上的所有生命周期方法,但是我們想在跳轉到該頁時才真正構造數據視圖和請求數據。那么我們可以使用一個占位視圖,那么可以想到使用ViewStub,當真正跳轉到該頁時,執行ViewStub.inflate()方法,加載真正的數據視圖和請求數據。
視圖保存
當某一頁超出可視范圍和預加載范圍,那么它將會被銷毀,FragmentStatePagerAdapter銷毀整個Fragment, 我們可以自己保存該Fragment,或使用FragmentPagerAdapter讓FragmentTransition來保留Fragment的引用。雖然這樣,但是它的周期方法已經走完,那么我們只能手動的保存Fragment根View的引用,當再次重新進入新的聲明周期方法時,返回原來的View
是否已經被用戶所看到
其實本身而言,FragmentManager并沒有提供為Fragment被用戶所看到的回調方法,而是在FragmentPagerAdapter和FragmentStatePagerAdapter中,調用了Fragment.setUserVisibleHint(boolean)來表明Fragment是否已經被作為primaryFragment. 所以這個方法可以被認為是一個回調方法。
06.如何實現預加載機制主要的方法是Fragment中的setUserVisibleHint(),此方法會在onCreateView()之前執行,當viewPager中fragment改變可見狀態時也會調用,當fragment 從可見到不見,或者從不可見切換到可見,都會調用此方法,使用getUserVisibleHint() 可以返回fragment是否可見狀態。
在BaseLazyFragment中需要在onActivityCreated()及setUserVisibleHint()方法中都調了一次lazyLoad() 方法。如果僅僅在setUserVisibleHint()調用lazyLoad(),當默認首頁首先加載時會導致viewPager的首頁第一次展示時沒有數據顯示,切換一下才會有數據。因為首頁fragment的setUserVisible()在onActivityCreated() 之前調用,此時isPrepared為false 導致首頁fragment 沒能調用onLazyLoad()方法加載數據。
/** ** @author yangchong * blog : https://github.com/yangchong211 * time : 2017/7/22 * desc : 懶加載 * revise: 懶加載時機:onCreateView()方法執行完畢 + setUserVisibleHint()方法返回true*/ public abstract class BaseLazyFragment extends BaseFragment { /* * 預加載頁面回調的生命周期流程: * setUserVisibleHint() -->onAttach() --> onCreate()-->onCreateView()--> * onActivityCreate() --> onStart() --> onResume() */ /** * 懶加載過 */ protected boolean isLazyLoaded = false; /** * Fragment的View加載完畢的標記 */ private boolean isPrepared = false; /** * 第一步,改變isPrepared標記 * 當onViewCreated()方法執行時,表明View已經加載完畢,此時改變isPrepared標記為true,并調用lazyLoad()方法 */ @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); isPrepared = true; //只有Fragment onCreateView好了 //另外這里調用一次lazyLoad() lazyLoad(); } /** * 第二步 * 此方法會在onCreateView()之前執行 * 當viewPager中fragment改變可見狀態時也會調用 * 當fragment 從可見到不見,或者從不可見切換到可見,都會調用此方法 * true表示當前頁面可見,false表示不可見 */ @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); LogUtil.d("setUserVisibleHint---"+isVisibleToUser); //只有當fragment可見時,才進行加載數據 if (isVisibleToUser){ lazyLoad(); } } /** * 調用懶加載 * 第三步:在lazyLoad()方法中進行雙重標記判斷,通過后即可進行數據加載 */ private void lazyLoad() { if (getUserVisibleHint() && isPrepared && !isLazyLoaded) { showFirstLoading(); onLazyLoad(); isLazyLoaded = true; } else { //當視圖已經對用戶不可見并且加載過數據,如果需要在切換到其他頁面時停止加載數據,可以覆寫此方法 if (isLazyLoaded) { stopLoad(); } } } /** * 視圖銷毀的時候講Fragment是否初始化的狀態變為false */ @Override public void onDestroyView() { super.onDestroyView(); isLazyLoaded = false; isPrepared = false; } /** * 第一次可見時,操作該方法,可以用于showLoading操作,注意這個是全局加載loading */ protected void showFirstLoading() { LogUtil.i("第一次可見時show全局loading"); } /** * 停止加載 * 當視圖已經對用戶不可見并且加載過數據,但是沒有加載完,而只是加載loading。 * 如果需要在切換到其他頁面時停止加載數據,可以覆寫此方法。 * 存在問題,如何停止加載網絡 */ protected void stopLoad(){ } /** * 第四步:定義抽象方法onLazyLoad(),具體加載數據的工作,交給子類去完成 */ @UiThread protected abstract void onLazyLoad(); } ```
onLazyLoad()加載數據條件
getUserVisibleHint()會返回是否可見狀態,這是fragment實現懶加載的關鍵,只有fragment 可見才會調用onLazyLoad() 加載數據。
isPrepared參數在系統調用onActivityCreated時設置為true,這時onCreateView方法已調用完畢(一般我們在這方法里執行findviewbyid等方法),確保 onLazyLoad()方法不會報空指針異常。
isLazyLoaded確保ViewPager來回切換時BaseFragment的initData方法不會被重復調用,onLazyLoad在該Fragment的整個生命周期只調用一次,第一次調用onLazyLoad()方法后馬上執行 isLazyLoaded = true。
然后再繼承這個BaseLazyFragment實現onLazyLoad() 方法就行。他會自動控制當fragment 展現出來時,才會加載數據
還有幾個細節需要優化一下
當視圖已經對用戶不可見并且加載過數據,如果需要在切換到其他頁面時停止加載數據,可以覆寫此方法,也就是stopLoad
視圖銷毀的時候講Fragment是否初始化的狀態變為false,這個也需要處理一下
第一次可見時,定義一個showFirstLoading方法,操作該方法,可以用于Loading加載操作,注意這個是全局加載loading,和下拉刷新數據或者局部刷新的loading不一樣的。可能有些開發app,沒有將loading分的這么細。
07.懶加載配合狀態管理器
什么是狀態管理器?
一般在需要用戶等待的場景,顯示一個Loading動畫可以讓用戶知道App正在加載數據,而不是程序卡死,從而給用戶較好的使用體驗。
當加載的數據為空時顯示一個數據為空的視圖、在數據加載失敗時顯示加載失敗對應的UI并支持點擊重試會比白屏的用戶體驗更好一些。
加載中、加載失敗、空數據的UI風格,一般來說在App內的所有頁面中需要保持一致,也就是需要做到全局統一。
如何降低偶性和入侵性
讓View狀態的切換和Activity徹底分離開,必須把這些狀態View都封裝到一個管理類中,然后暴露出幾個方法來實現View之間的切換。
在不同的項目中可以需要的View也不一樣,所以考慮把管理類設計成builder模式來自由的添加需要的狀態View。 - 那么如何降低耦合性,讓代碼入侵性低。方便維護和修改,且移植性強呢?大概具備這樣的條件…… - 可以運用在activity或者fragment中 - 不需要在布局中添加LoadingView,而是統一管理不同狀態視圖,同時暴露對外設置自定義狀態視圖方法,方便UI特定頁面定制 - 支持設置自定義不同狀態視圖,即使在BaseActivity統一處理狀態視圖管理,也支持單個頁面定制 - 在加載視圖的時候像異常和空頁面能否用ViewStub代替,這樣減少繪制,只有等到出現異常和空頁面時,才將視圖給inflate出來 - 當頁面出現網絡異常頁面,空頁面等,頁面會有交互事件,這時候可以設置點擊設置網絡或者點擊重新加載等等
那么具體怎么操作呢?
可以自由切換內容,空數據,異常錯誤,加載,網絡錯誤等5種狀態。父類BaseFragment直接暴露5中狀態,方便子類統一管理狀態切換,這里fragment的封裝和activity差不多。
/** ** @author yangchong * blog : https://github.com/yangchong211 * time : 2017/7/20 * desc : fragment的父類 * revise: 注意,該類具有懶加載 **/ public abstract class BaseStateFragment extends BaseLazyFragment { protected StateLayoutManager statusLayoutManager; private View view; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { if(view==null){ view = inflater.inflate(R.layout.base_state_view, container , false); initStatusLayout(); initBaseView(view); } return view; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); initView(view); initListener(); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); } /** * 獲取到子布局 * @param view view */ private void initBaseView(View view) { LinearLayout llStateView = view.findViewById(R.id.ll_state_view); llStateView.addView(statusLayoutManager.getRootLayout()); } /** * 初始化狀態管理器相關操作 */ protected abstract void initStatusLayout(); /** * 初始化View的代碼寫在這個方法中 * @param view view */ public abstract void initView(View view); /** * 初始化監聽器的代碼寫在這個方法中 */ public abstract void initListener(); /** * 第一次可見狀態時,showLoading操作,注意下拉刷新操作時不要用該全局loading */ @Override protected void showFirstLoading() { super.showFirstLoading(); showLoading(); } /*protected void initStatusLayout() { statusLayoutManager = StateLayoutManager.newBuilder(activity) .contentView(R.layout.common_fragment_list) .emptyDataView(R.layout.view_custom_empty_data) .errorView(R.layout.view_custom_data_error) .loadingView(R.layout.view_custom_loading_data) .netWorkErrorView(R.layout.view_custom_network_error) .build(); }*/ /*---------------------------------下面是狀態切換方法-----------------------------------------*/ /** * 加載成功 */ protected void showContent() { if (statusLayoutManager!=null){ statusLayoutManager.showContent(); } } /** * 加載無數據 */ protected void showEmptyData() { if (statusLayoutManager!=null){ statusLayoutManager.showEmptyData(); } } /** * 加載異常 */ protected void showError() { if (statusLayoutManager!=null){ statusLayoutManager.showError(); } } /** * 加載網絡異常 */ protected void showNetWorkError() { if (statusLayoutManager!=null){ statusLayoutManager.showNetWorkError(); } } /** * 加載loading */ protected void showLoading() { if (statusLayoutManager!=null){ statusLayoutManager.showLoading(); } } } //如何切換狀態呢? showContent(); showEmptyData(); showError(); showLoading(); showNetWorkError(); //或者這樣操作也可以 statusLayoutManager.showLoading(); statusLayoutManager.showContent();
狀態管理器的設計思路
StateFrameLayout是繼承FrameLayout自定義布局,主要是存放不同的視圖,以及隱藏和展示視圖操作
StateLayoutManager是狀態管理器,主要是讓開發者設置不同狀態視圖的view,以及切換視圖狀態操作
幾種異常狀態要用ViewStub,因為在界面狀態切換中loading和內容View都是一直需要加載顯示的,但是其他的3個只有在沒數據或者網絡異常的情況下才會加載顯示,所以用ViewStub來加載他們可以提高性能。
OnRetryListener,為接口,主要是重試作用。比如加載失敗了,點擊視圖需要重新刷新接口,則可以用到這個。開發者也可以自己設置點擊事件
關于狀態視圖切換方案,目前市場有多種做法,具體可以看我的這篇博客:https://juejin.im/post/5d2f01...
其他介紹 01.關于博客匯總鏈接1.技術博客匯總
2.開源項目匯總
3.生活博客匯總
4.喜馬拉雅音頻匯總
5.其他匯總
02.關于我的博客github:https://github.com/yangchong211
知乎:https://www.zhihu.com/people/...
簡書:http://www.jianshu.com/u/b7b2...
csdn:http://my.csdn.net/m0_37700275
喜馬拉雅聽書:http://www.ximalaya.com/zhubo...
開源中國:https://my.oschina.net/zbj161...
泡在網上的日子:http://www.jcodecraeer.com/me...
郵箱:yangchong211@163.com
阿里云博客:https://yq.aliyun.com/users/a... 239.headeruserinfo.3.dT4bcV
segmentfault頭條:https://segmentfault.com/u/xi...
掘金:https://juejin.im/user/593943...
項目地址:https://github.com/yangchong2...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/75578.html
摘要:前言最近發布了版本,新增功能,該功能在上并不友好,現在官方將此功能延續下來,這回是騾子是馬呢趕緊拉出來溜溜閱讀指南內容基于版本講解,由于正式版還未發布,如有功能變動有勞看官指出內容重點介紹的特性和預加載機制,另外包括的狀態和的生命周前言 最近ViewPager2發布了1.0.0-alpha04版本,新增offscreenPageLimit功能,該功能在ViewPager上并不友好,現在官方將...
摘要:如果是那么在初始狀態下,默認會出現前兩個頁面,而主頁面是在的起始位置通常是屏幕左側,直到最后一個頁面在屏幕右側,如果總共個頁面,返回值為那么將一次性出現所有的頁面用于數據刷新時的頁面處理方式。 目錄介紹 01.PagerAdapter簡單介紹02.PagerAdapter抽象方法03.PagerAdapter原理介紹04.PagerAdapter緩存和銷毀05.自定義PagerAdap...
摘要:也有類似的棧,稱為回退棧,回退棧是由管理的。為的參數,通過能找到回退棧的特定元素,可以為或者,表示只彈出該元素以上的所有元素,表示彈出包含該元素及以上的所有元素。是異步執行的,是丟到主線程的執行,是同步版本。 歡迎大家前往云+社區,獲取更多騰訊海量技術實踐干貨哦~ 由 天天P圖攻城獅 發布在云+社區 作者簡介:damonxia(夏正冬),天天P圖Android工程師 下文中Demo的源...
閱讀 3164·2019-08-30 15:55
閱讀 2945·2019-08-30 13:46
閱讀 1446·2019-08-29 17:29
閱讀 3513·2019-08-29 11:08
閱讀 3438·2019-08-29 11:04
閱讀 1088·2019-08-28 18:20
閱讀 545·2019-08-26 13:37
閱讀 1327·2019-08-26 11:49