摘要:前言最近發布了版本,新增功能,該功能在上并不友好,現在官方將此功能延續下來,這回是騾子是馬呢趕緊拉出來溜溜閱讀指南內容基于版本講解,由于正式版還未發布,如有功能變動有勞看官指出內容重點介紹的特性和預加載機制,另外包括的狀態和的生命周
前言最近ViewPager2發布了1.0.0-alpha04版本,新增offscreenPageLimit功能,該功能在ViewPager上并不友好,現在官方將此功能延續下來,這回是騾子是馬呢?趕緊拉出來溜溜;
閱讀指南:
內容基于ViewPager21.0.0-alpha04版本講解,由于正式版還未發布,如有功能變動有勞看官指出
內容重點:介紹ViewPager2的offscreenPageLimit特性和預加載機制,另外包括Adapter的狀態和Fragment的生命周期等內容
ViewPager頑疾頑疾是什么鬼,沒有這么嚴重吧。ViewPager有兩個毛?。?b>不能關閉預加載和更新Adapter不生效,所以開頭我為什么說offscreenPageLimit在ViewPager上十分不友好;本質上是因為offscreenPageLimit不能設置成0(設置成0就是想象中的關閉預加載);
上面是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使用時依然存在
Fragment懶加載前因后果先說PagerAdapter:
PagerAdapter常用方法如下:
instantiateItem(ViewGroup container, int position)初始化ItemView,返回需要添加ItemView
destroyItem(iewGroup container, int position, Object object)銷毀ItemView,移除指定的ItemView
isViewFromObject(View view, Object object)View和Object是否對應
setPrimaryItem(ViewGroup container, int position, Object object) 當前頁面的主Item
getCount()獲取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狀態,控制數據延遲加載,而布局只能提前進入;
ViewPager2基本使用build.gradle引入
implementation androidx.viewpager2:viewpager2:1.0.0-alpha04
布局文件添加
"@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
設置ViewHolder+Adapter
ViewPager2 viewPager = findViewById(R.id.view_pager2);
viewPager.setAdapter(new RecyclerView.Adapter() {
@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);
}
}
設置Fragment+Adapter
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的預加載;
ViewPager2離屏加載在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預加載和緩存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上;
提前加載對Adapter影響所謂的提前加載,是指當前position不可見但加載了布局,包括上面說的預加載和離屏加載,下面先介紹一下Adapter:
ViewPager2的Adapter本質上是RecyclerView.Adapter,下面列舉常用方法:
onCreateViewHolder(ViewGroup parent, int viewType)創建ViewHolder
onBindViewHolder(VH holder, int position)綁定ViewHolder
onViewRecycled(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支持目前,ViewPager2對Fragment的支持只能使用FragmentStateAdapter,使用起來也是非常簡單:
默認情況下,ViewPager2是開啟預加載關閉離屏加載的,這種情況下,切換頁面對Fragment生命周如何?
問題一:關閉預加載對Fragment的影響: 經過驗證,是否開啟預加載,對Fragment的生命周期沒有影響,結果和默認上圖是一樣的;
問題二:開啟離屏加載對Fragment的影響: 設置offscreenPageLimit=1時:
打印結果解讀:
備注:log日志下標是從2開始的,標注的頁碼是從1開始,請自行矯正;
默認情況下,ViewPager2會緩存兩條數據,所以滑動到第4頁,第1頁的Fragment才開始移除,這可以理解;
設置offscreenPageLimit=1時,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個;
FragmentStateAdapter源碼簡單解讀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對象會提前創建;
如果當前的holder.itemView已經添加到屏幕且已經布局且parent不等于空,就會將Fragment綁定到ViewHodler;
每次調用都會gc一次,主要的避免用戶修改數據源造成垃圾對象;
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源碼解讀盡請期待;
but!!!Fragment中監聽不到setUserVisibleHint
在設置offscreenPageLimit>0時,Fragment中是監聽不到setUserVisibleHint調用的,我查了源碼沒有調用,而且該方法被標記過時,所以,適用于ViewPager那一套懶加載Fragment在這里恐怕是不行了;
話又說回來,既然想玩懶加載,為啥還要設置offscreenPageLimit>0呢,offscreenPageLimit=0就自帶懶加載效果;
Adapter小結:目前ViewPager2對Fragment支持只能用FragmentStateAdapter,FragmentStateAdapter在遇到預加載時,只會創建Fragment對象,不會把Fragment真正的加入到布局中,所以自帶懶加載效果;
FragmentStateAdapter不會一直保留Fragment實例,回收的ItemView也會移除Fragment,所以得做好Fragment`重建后恢復數據的準備;
FragmentStateAdapter在遇到offscreenPageLimit>0時,處理離屏Fragment和可見Fragment沒有什么區別,所以無法通過setUserVisibleHint判斷顯示與否,這一點知得注意;
ViewPager懶加載請注意新版的Fragment中(Version 1.1.0-alpha07),該方法setUserVisibleHint已經過時,由FragmentTransactionsetMaxLifecycle替代,新版本的FragmentPagerAdapter可以設置直接調用生命周期,這代表ViewPager+Fragment懶加載有更好的解決方案,請注意
最后 ViewPager2更多優點由于本章篇幅有點,沒有對ViewPager2進行的全面介紹,不代表ViewPager就僅此而已,就當前版本來看,ViewPager2的優點或者特有的功能如下:
支持RecyclerView級別的復用
支持預加載和離屏加載(本章介紹)
支持動態更新Adapter(ViewPager大坑之一)
支持模擬拖拽
支持豎直方向滑動
支持頁面滑動狀態監聽和頁面變換(延續了ViewPager的功能)
只想到這么多了
總結這一次ViewPager2更新,官方貌似要發力替換ViewPager了,無論是它高效的復用還是自帶懶加載,亦或是更新有效的Adapter,都要比ViewPager強大,如果看官老爺們想嘗試升級,在下十分贊賞,但從當前版本來看,請謹慎使用Fragment+offscreenPageLimit>0組合的情況。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/7041.html
摘要:前言最近我在關注的使用,期間一直基于官方的調試,今天遇到一個奇葩的問題,捉摸了半天最終找到原因,原來是中布局的問題,事后感覺有必要分享一下這個過程,一來可以鞏固測量的知識,二來希望大家能避開這個坑閱讀指南代碼基于,看官老爺最好能下載前言 最近我在關注ViewPager2的使用,期間一直基于官方的Demo調試android-viewpager2,今天遇到一個奇葩的問題,捉摸了半天最終找到原因,...
摘要:重大更新亮點解讀月日對來說是個特別的日子不僅是項目四周年紀念日,也是經過了一年密集開發之后發布內測版的日子。是一次重大更新,幾乎涉及每行代碼。 Bootstrap 4重大更新、亮點解讀 8月19日對Bootstrap來說是個特別的日子——不僅是項目四周年紀念日,也是經過了一年密集開發之后發布Bootstrap 4內測版的日子。Bootstrap 4是一次重大更新,幾乎涉及每行代碼。 新...
閱讀 713·2023-04-25 19:43
閱讀 3910·2021-11-30 14:52
閱讀 3784·2021-11-30 14:52
閱讀 3852·2021-11-29 11:00
閱讀 3783·2021-11-29 11:00
閱讀 3869·2021-11-29 11:00
閱讀 3558·2021-11-29 11:00
閱讀 6105·2021-11-29 11:00