最近ViewPager2
發佈了1.0.0-alpha04
版本,新增offscreenPageLimit
功能,該功能在ViewPager
上並不友好,如今官方將此功能延續下來,這回是騾子是馬呢?趕忙拉出來溜溜;android
閱讀指南:git
1.0.0-alpha04
版本講解,因爲正式版還未發佈,若有功能變更有勞看官指出offscreenPageLimit
特性和預加載
機制,另外包括Adapter的狀態和Fragment的生命週期等內容頑疾是什麼鬼,沒有這麼嚴重吧。ViewPager
有兩個毛病:不能關閉預加載
和更新Adapter不生效
,因此開頭我爲何說offscreenPageLimit
在ViewPager
上十分不友好;本質上是由於offscreenPageLimit
不能設置成0(設置成0就是想象中的關閉預加載);github
上面是ViewPager默認狀況下的加載示意圖,當切換到當前頁面時,會默認預加載左右兩側的佈局到ViewPager
中,儘管兩側的View並不可見的,咱們稱這種狀況叫預加載
;因爲ViewPager
對offscreenPageLimit
設置了限制,頁面的預加載是不可避免;數組
ViewPager緩存
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
public void setOffscreenPageLimit(int limit) {
if (limit < DEFAULT_OFFSCREEN_PAGES) {//不容許小於1
Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
+ DEFAULT_OFFSCREEN_PAGES);
limit = DEFAULT_OFFSCREEN_PAGES;
}
if (limit != mOffscreenPageLimit) {
mOffscreenPageLimit = limit;
populate();
}
}
複製代碼
ViewPager強制預加載的邏輯在Fragment
配合ViewPager
使用時依然存在bash
先說PagerAdapter
:app
PagerAdapter
經常使用方法以下:ide
instantiateItem(ViewGroup container, int position)
初始化ItemView,返回須要添加ItemViewdestroyItem(iewGroup container, int position, Object object)
銷燬ItemView,移除指定的ItemViewisViewFromObject(View view, Object object)
View和Object是否對應setPrimaryItem(ViewGroup container, int position, Object object)
當前頁面的主ItemgetCount()
獲取Item個數先說setPrimaryItem(ViewGroup container, int position, Object object)
,該方法表示當前頁面正在顯示主要Item
,何爲主要Item
?若是預加載的ItemView已經劃入屏幕,當前的PrimaryItem
依然不會改變,除非新的ItemView徹底劃入屏幕,且滑動已經中止纔會判斷;工具
因爲ViewPager
不可避免的進行佈局預加載,形成PagerAdapter
必須提早調用instantiateItem(ViewGroup container, int position)
方法,instantiateItem()
是建立ItemView的惟一入口方法,因此PagerAdapter
的實現類FragmentPagerAdapter
和FragmentStatePagerAdapter
必須抓住該方法進行Fragment
對象的建立;佈局
碰巧的是,FragmentPagerAdapter
和FragmentStatePagerAdapter
一股腦的在instantiateItem()
中進行建立且進行add
或attach
操做,並無在setPrimaryItem()
方法中對Fragment
進行操做;
所以,預加載會致使不可見的Fragment
一股腦的調用onCreate
、onCreateView
、onResume
等方法,用戶只能經過Fragment.setUserVisibleHint()
方法進行識別;
大多數的懶加載都是對Fragment
作手腳,結合生命週期方法和setUserVisibleHint
狀態,控制數據延遲加載,而佈局只能提早進入;
implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha04'
複製代碼
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
複製代碼
ViewPager2 viewPager = findViewById(R.id.view_pager2);
viewPager.setAdapter(new RecyclerView.Adapter<ViewHolder>() {
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_card_layout, parent, false);
ViewHolder viewHolder = new ViewHolder(itemView);
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.labelCenter.setText(String.valueOf(position));
}
@Override
public int getItemCount() {
return SIZE;
}
}));
static class ViewHolder extends RecyclerView.ViewHolder{
private final TextView labelCenter;
public ViewHolder(@NonNull View itemView) {
super(itemView);
labelCenter = itemView.findViewById(R.id.label_center);
}
}
複製代碼
viewPager.setAdapter(new FragmentStateAdapter(this) {
@NonNull
@Override
public Fragment getItem(int position) {
return new VSFragment();
}
@Override
public int getItemCount() {
return SIZE;
}
});
複製代碼
ViewPager2
的使用很是簡單,甚至比ViewPager
還要簡單,只要熟悉RecyclerView
的童鞋確定會寫ViewPager2
;
ViewPager2
經常使用方法以下:
setAdapter()
設置適配器setOrientation()
設置佈局方向setCurrentItem()
設置當前Item下標beginFakeDrag()
開始模擬拖拽fakeDragBy()
模擬拖拽中endFakeDrag()
模擬拖拽結束setUserInputEnabled()
設置是否容許用戶輸入/觸摸setOffscreenPageLimit()
設置屏幕外加載頁面數量registerOnPageChangeCallback()
註冊頁面改變回調setPageTransformer()
設置頁面滑動時的變換效果不少好看好玩的效果,請讀者自行運行官方的DEMO(github.com/googlesampl…);
在上文說ViewPager
預加載時,我就在想offscreenPageLimit
能不能稱之爲預加載
,若是在ViewPager
上能夠,那麼在ViewPager2
上可能就要混淆了,由於ViewPager2
擁有RecyclerView
的一整套緩存策略,包括RecyclerView
的預加載;爲了不混淆,在下面的文章中我把offscreenPageLimit
定義爲離屏加載
,預加載
只表明RecyclerView
的預加載;
在1.0.0-alpha04
版本中,ViewPager2
提供了離屏加載功能,該功能和ViewPager
的預加載存的的意義彷佛是同樣的;
ViewPager2
public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = 0;
public void setOffscreenPageLimit(int limit) {
if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
throw new IllegalArgumentException(
"Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
}
mOffscreenPageLimit = limit;
// Trigger layout so prefetch happens through getExtraLayoutSize()
mRecyclerView.requestLayout();
}
複製代碼
從代碼能夠看出,ViewPager2
的離屏加載最小能夠爲0,僅僅從這一步開始,我大膽的猜想ViewPager2
支持所謂的懶加載
,帶着好奇,看一眼OffscreenPageLimit
實現原理;
ViewPager2.LinearLayoutManagerImpl
@Override
protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
@NonNull int[] extraLayoutSpace) {
int pageLimit = getOffscreenPageLimit();
if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {//若是等於默認值(0),調用基類的方法
// Only do custom prefetching of offscreen pages if requested
super.calculateExtraLayoutSpace(state, extraLayoutSpace);
return;
}
//返回offscreenSpace
final int offscreenSpace = getPageSize() * pageLimit;
extraLayoutSpace[0] = offscreenSpace;
extraLayoutSpace[1] = offscreenSpace;
}
複製代碼
OffscreenPageLimit
本質上是重寫LinearLayoutManager
的calculateExtraLayoutSpace
方法,該方法是最新的recyclerView
包加入的功能;
calculateExtraLayoutSpace
方法定義了佈局額外的空間,何爲佈局額外的空間?默認空間等於RecyclerView的寬高空間,定義這個意在能夠放大可佈局的空間,該方法參數extraLayoutSpace
是一個長度爲2的int數組,第一條數據接受左邊/上邊的額外空間,第二條數據接受右邊/下邊的額外空間,故上訴代碼是代表左右/上下各擴大offscreenSpace
;
綜上代碼,OffscreenPageLimit
其實就是放大了LinearLayoutManager
的佈局空間,咱們下面看運行效果;
爲了對比二者加載佈局的效果,我準備了LinearLayout同時展現ViewPager和ViewPager2,設置相同的Item佈局和數據源,而後用Android佈局分析工具抓取二者的佈局結構,代碼比較簡單,就不貼出來了;
offscreenPageLimit
從運行結果來看,ViewPager
會默認會預佈局
兩側各一個佈局,ViewPager2
默認不進行預佈局
,主要由各自的默認offscreenPageLimit
參數決定,ViewPager
默認爲1且不容許小於1,ViewPager2
默認爲0
offscreenPageLimit=2
分析運行結果,在設置相同的offscreenPageLimit
時,二者都會預佈局左右(上下)二者的offscreenPageLimit
個ItemView;
從對比結果上來看,ViewPager2
的offscreenPageLimit
和ViewPager
運行結果同樣,可是ViewPager2
最小offscreenPageLimit
能夠設置爲0;
ViewPager2預加載
即RecyclerView
的預加載,代碼在RecyclerView
的GapWorker
中,這個知識可能有些同窗不是很瞭解,推薦先看這篇博客medium.com/google-deve…;
在ViewPager2
上默認開啓預加載,表現形式是在拖動控件或者Fling
時,可能會預加載一條數據;下面是預加載的示意圖:
如何關閉預加載?
((RecyclerView)viewPager.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);
複製代碼
預加載的開關在LayoutManager
上,只須要獲取LayoutManager
並調用setItemPrefetchEnabled()
便可控制開關;
ViewPager2
默認會緩存2條ItemView
,並且在最新的RecyclerView
中能夠自定義緩存Item的個數;
RecyclerView
public void setItemViewCacheSize(int size) {
mRecycler.setViewCacheSize(size);
}
複製代碼
小結: 預加載
和緩存
在View
層面沒有本質的區別,都是已經準備了佈局,可是沒有加載到parent上; 預加載
和離屏加載
在View
層面有本質的區別,離屏加載
的View已經添加到parent上;
所謂的提早加載,是指當前position
不可見但加載了佈局,包括上面說的預加載
和離屏加載
,下面先介紹一下Adapter
:
ViewPager2
的Adapter
本質上是RecyclerView.Adapter
,下面列舉經常使用方法:
onCreateViewHolder(ViewGroup parent, int viewType)
建立ViewHolderonBindViewHolder(VH holder, int position)
綁定ViewHolderonViewRecycled(VH holder)
當View被回收onViewAttachedToWindow(VH holder)
當前View加載到窗口onViewDetachedFromWindow(VH holder)
當前View從窗口移除getItemCount()
//獲取Item個數下面主要針對ItemView
的建立來講,暫不討論回收的狀況;
onBindViewHolder
預加載和離屏加載都會調用onViewAttachedToWindow
離屏加載ItemView會調用,可見ItemView會調用onViewDetachedFromWindow
從可見到不可見的ItemView(除離屏中)一定調用小結: 預加載
和緩存
在Adapter
層面沒有區別,都會調用onBindViewHolder
方法; 預加載
和離屏加載
在Adapter
層面有本質的區別,離屏加載
的View會調用onViewAttachedToWindow
;
目前,ViewPager2
對Fragment
的支持只能使用FragmentStateAdapter
,使用起來也是很是簡單:
默認狀況下,ViewPager2
是開啓預加載
關閉離屏加載
的,這種狀況下,切換頁面對Fragment生命周如何?
問題一:關閉預加載對Fragment
的影響: 通過驗證,是否開啓預加載,對Fragment
的生命週期沒有影響,結果和默認上圖是同樣的;
問題二:開啓離屏加載對Fragment
的影響: 設置offscreenPageLimit=1時:
打印結果解讀:
備註:log日誌下標是從2開始的,標註的頁碼是從1開始,請自行矯正;
ViewPager2
會緩存兩條數據,因此滑動到第4頁,第1頁的Fragment纔開始移除,這能夠理解;ViewPager2
在第1頁會加載兩條數據,這能夠理解,會把下一頁View提早加載進來;之後每滑一頁,會加載下一頁數組,直到第5頁,會移除第1頁的Fragment
;第6頁會移除第2頁的Fragment
如何理解offscreenPageLimit
對Fragment
的影響,假設offscreenPageLimit=1,這樣ViewPager2最多能夠承託3個ItemView,再加上2個緩存的ItemView,就是5個,因爲offscreenPageLimit會在ViewPager2兩邊放置一個,因此向前最多承載4個,向後最多能承載1個(預加載對Fragment沒有影響,因此不計算),這樣很天然就是第5個時候,回收第1個;
onCreateViewHolder()方法
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return FragmentViewHolder.create(parent);
}
static FragmentViewHolder create(ViewGroup parent) {
FrameLayout container = new FrameLayout(parent.getContext());
container.setLayoutParams(
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
container.setId(ViewCompat.generateViewId());
container.setSaveEnabled(false);
return new FragmentViewHolder(container);
}
複製代碼
onCreateViewHolder()
建立一個寬高都MATCH_PARENT
的FrameLayout
,注意這裏並不像PagerAdapter
是Fragment
的rootView
;
onBindViewHolder()
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
final long itemId = holder.getItemId();
final int viewHolderId = holder.getContainer().getId();
final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
if (boundItemId != null && boundItemId != itemId) {
removeFragment(boundItemId);
mItemIdToViewHolder.remove(boundItemId);
}
mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
//保證目標Fragment不爲空,意思是能夠提早建立
ensureFragment(position);
/** Special case when {@link RecyclerView} decides to keep the {@link container}
* attached to the window, but not to the view hierarchy (i.e. parent is null) */
final FrameLayout container = holder.getContainer();
//若是ItemView已經在添加到Window中,且parent不等於null,會觸發綁定viewHoder操做;
if (ViewCompat.isAttachedToWindow(container)) {
if (container.getParent() != null) {
throw new IllegalStateException("Design assumption violated.");
}
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (container.getParent() != null) {
container.removeOnLayoutChangeListener(this);
//將Fragment和ViewHolder綁定
placeFragmentInViewHolder(holder);
}
}
});
}
//回收垃圾Fragments
gcFragments();
}
複製代碼
onBindViewHolder()
首先會獲取當前position對應的Fragment
,這意味着預加載的Fragment
對象會提早建立;onViewAttachedToWindow()
public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
placeFragmentInViewHolder(holder);
gcFragments();
}
複製代碼
onViewAttachedToWindow()
方法調用onViewAttachedToWindow
將Fragment
和hodler
綁定;
onViewRecycled()
public final void onViewRecycled(@NonNull FragmentViewHolder holder) {
final int viewHolderId = holder.getContainer().getId();
final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
if (boundItemId != null) {
removeFragment(boundItemId);
mItemIdToViewHolder.remove(boundItemId);
}
}
複製代碼
當onViewRecycled()
時纔會觸發Fragment
移除;
核心添加操做:
//將Fragment.rootView添加到FrameLayout;
scheduleViewAttach(fragment, container);//將rootI
mFragmentManager.beginTransaction().add(fragment, "f" + holder.getItemId()).commitNow();
//主要是監聽onFragmentViewCreated方法,獲取rootView而後添加到container
private void scheduleViewAttach(final Fragment fragment, final FrameLayout container) {
// After a config change, Fragments that were in FragmentManager will be recreated. Since
// ViewHolder container ids are dynamically generated, we opted to manually handle
// attaching Fragment views to containers. For consistency, we use the same mechanism for
// all Fragment views.
mFragmentManager.registerFragmentLifecycleCallbacks(
new FragmentManager.FragmentLifecycleCallbacks() {
@Override
public void onFragmentViewCreated(@NonNull FragmentManager fm,
@NonNull Fragment f, @NonNull View v,
@Nullable Bundle savedInstanceState) {
if (f == fragment) {
fm.unregisterFragmentLifecycleCallbacks(this);
addViewToContainer(v, container);
}
}
}, false);
}
複製代碼
更詳細的FragmentStateAdapter源碼解讀盡請期待;
Fragment
中監聽不到setUserVisibleHint
在設置offscreenPageLimit>0時,Fragment
中是監聽不到setUserVisibleHint
調用的,我查了源碼沒有調用,並且該方法被標記過期,因此,適用於ViewPager
那一套懶加載Fragment
在這裏恐怕是不行了;
話又說回來,既然想玩懶加載,爲啥還要設置offscreenPageLimit>0呢,offscreenPageLimit=0就自帶懶加載效果;
ViewPager2
對Fragment
支持只能用FragmentStateAdapter
,FragmentStateAdapter
在遇到預加載
時,只會建立Fragment
對象,不會把Fragment
真正的加入到佈局中,因此自帶懶加載效果;FragmentStateAdapter
不會一直保留Fragment
實例,回收的ItemView
也會移除Fragment
,因此得作好Fragment`重建後恢復數據的準備;FragmentStateAdapter
在遇到offscreenPageLimit>0時,處理離屏Fragment
和可見Fragment
沒有什麼區別,因此沒法經過setUserVisibleHint
判斷顯示與否,這一點知得注意;新版的Fragment中(Version 1.1.0-alpha07
),該方法setUserVisibleHint
已通過時,由FragmentTransactionsetMaxLifecycle
替代,新版本的FragmentPagerAdapter
能夠設置直接調用生命週期,這表明ViewPager+Fragment懶加載有更好的解決方案,請注意
因爲本章篇幅有點,沒有對ViewPager2
進行的全面介紹,不表明ViewPager
就僅此而已,就當前版原本看,ViewPager2
的優勢或者特有的功能以下:
這一次ViewPager2
更新,官方貌似要發力替換ViewPager
了,不管是它高效的複用
仍是自帶懶加載
,亦或是更新有效的Adapter
,都要比ViewPager
強大,若是看官老爺們想嘗試升級,在下十分讚揚,但從當前版原本看,請謹慎使用Fragment
+offscreenPageLimit>0
組合的狀況。