写点什么

ViewPager2 重大更新,支持 offscreenPageLimit

用户头像
Android架构
关注
发布于: 30 分钟前

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 的布局空间,我们下面看运行效果;


布局对比




为了对比两者加载布局的效果


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


,我准备了 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 绑定

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
ViewPager2重大更新,支持offscreenPageLimit