ViewPager2 重大更新,支持 offscreenPageLimit
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'
布局文件添加
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
设置 ViewHolder+Adapter
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);
}
}
设置 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
https://github.com/googlesamples/android-viewpager2
注意
在上文说 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 中,这个知识可能有些同学不是很了解,推荐先看这篇博客
https://medium.com/google-developers/recyclerview-prefetch-c2f269075710
在 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 绑定
评论