最近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
组合的状况。