乐趣区

ViewPager2重大更新支持offscreenPageLimit

前言

最近 ViewPager2 发布了 1.0.0-alpha04 版本,新增 offscreenPageLimit 功能,该功能在 ViewPager 上并不友好,以下是官方对于 ViewPager2 的介绍:

众所周知,ViewPager 有两个毛病:不能关闭预加载和更新 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 懒加载。先说一下 ViewPager 配合 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

使用 ViewPager2 之前,需要先引入 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" />

ViewPager2 可以和 Adapter 一起使用,也可以和 Fragment 一起使用。

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);
    }
}

当然,ViewPager2 也可以和 Fragment 配合使用。

viewPager.setAdapter(new FragmentStateAdapter(this) {
        @NonNull
        @Override
        public Fragment getItem(int position) {return new VSFragment();
        }

        @Override
        public int getItemCount() {return SIZE;}
    });

ViewPager2 和 ViewPager 在使用方式上差不多,主要有以下一些 API:

  • setAdapter() 设置适配器
  • setOrientation() 设置布局方向
  • setCurrentItem() 设置当前 Item 下标
  • beginFakeDrag() 开始模拟拖拽
  • fakeDragBy() 模拟拖拽中
  • endFakeDrag() 模拟拖拽结束
  • setUserInputEnabled() 设置是否允许用户输入 / 触摸
  • setOffscreenPageLimit()设置屏幕外加载页面数量
  • registerOnPageChangeCallback() 注册页面改变回调
  • setPageTransformer() 设置页面滑动时的变换效果

离屏加载

在 1.0.0-alpha04 版本中,ViewPager2 提供了离屏加载功能,该功能和 ViewPager 的预加载存的的意义似乎是一样的。

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 和 ViewPager 的区别,我们看一下布局方面的对比:

布局对比

为了对比两者加载布局的效果,我准备了 LinearLayout 同时展示 ViewPager 和 ViewPager2,设置相同的 Item 布局和数据源,然后用 Android 布局分析工具抓取两者的布局结构,效果如下:

从分析结果来看,ViewPager 会默认会预布局两侧各一个布局,ViewPager2 默认不进行预布局,主要由各自的默认 offscreenPageLimit 参数决定,ViewPager 默认为 1 且不允许小于 1,ViewPager2 默认为 0。

分析运行结果,在设置相同的 offscreenPageLimit 时,两者都会预布局左右 (上下) 两者的 offscreenPageLimit 个 ItemView;
从对比结果上来看,ViewPager2 的 offscreenPageLimit 和 ViewPager 运行结果一样,但是 ViewPager2 最小 offscreenPageLimit 可以设置为 0;

预加载和缓存

ViewPager2 预加载即 RecyclerView 的预加载,代码在 RecyclerView 的 GapWorker 中。ViewPager2 会默认开启预加载,表现形式是在拖动控件或者 Fling 时,可能会预加载一条数据;下面是预加载的示意图:

如果要关闭预加载,可以使用下面的代码:

((RecyclerView)viewPager.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);

预加载的开关在 LayoutManager 上,只需要获取 LayoutManager 并调用 setItemPrefetchEnabled()即可控制开关;
ViewPager2 默认会缓存 2 条 ItemView,而且在最新的 RecyclerView 中可以自定义缓存 Item 的个数,方法如下:

public void setItemViewCacheSize(int size) {mRecycler.setViewCacheSize(size);
}

预加载和缓存在 View 层面没有本质的区别,都是已经准备了布局,但是没有加载到 parent 视图上;预加载和离屏加载在 View 层面有本质的区别,离屏加载的 View 已经添加到 parent 上;

预加载对 Fragment 的支持

目前,ViewPager2 对 Fragment 的支持只能使用 FragmentStateAdapter 来实现,使用起来也是非常简单,例如:

默认情况下,ViewPager2 是开启预加载关闭离屏加载的,这种情况下,切换页面对 Fragment 生命周期有何影响呢?以下是来自网络上的两个图:

问题一:关闭预加载对 Fragment 的影响 :经过验证,是否开启预加载,对 Fragment 的生命周期没有影响,结果和默认上图是一样的;
问题二:开启离屏加载对 Fragment 的影响 :设置 offscreenPageLimit= 1 时:

从打印日志可以看出,offscreenPageLimit= 1 时,ViewPager2 在第 1 页会加载两条数据,并且会把下一页 View 提前加载进来;以后每滑一页,会加载下一页数组,直到第 5 页,会移除第 1 页的 Fragment;第 6 页会移除第 2 页的 Fragment。

小结

针对此次的更新,ViewPager2 主要有以下一些特性:

  1. 目前 ViewPager2 对 Fragment 支持只能用 FragmentStateAdapter,FragmentStateAdapter 在遇到预加载时,只会创建 Fragment 对象,不会把 Fragment 真正的加入到布局中,所以自带懒加载效果;
  2. FragmentStateAdapter 不会一直保留 Fragment 实例,回收的 ItemView 也会移除 Fragment,所以得做好 Fragment 重建后恢复数据的准备;
  3. FragmentStateAdapter 在遇到 offscreenPageLimit>0 时,处理离屏 Fragment 和可见 Fragment 没有什么区别,所以无法通过 setUserVisibleHint 判断显示与否;

通过这次 ViewPager2 更新,官方貌似要发力替换 ViewPager 了,无论是它高效的复用还是自带懒加载,亦或是更新有效的 Adapter,都要比 ViewPager 更加强大。

退出移动版