ViewPager2重大更新,支持offscreenPageLimit

栏目: IOS · Android · 发布时间: 5年前

内容简介:最近为什么说上面是ViewPager默认情况下的加载示意图,当切换到当前页面时,会默认预加载左右两侧的布局到

最近 ViewPager2 发布了 1.0.0-alpha04 版本,新增 offscreenPageLimit 功能,该功能在 ViewPager 上并不友好,现在官方将此功能延续下来,这回是骡子是马呢?赶紧拉出来溜溜;

ViewPager2重大更新,支持offscreenPageLimit

阅读指南:

  • 基于ViewPager2 1.0.0-alpha04 版本讲解,由于正式版还未发布,如有功能变动还请读者指点
  • 本文主要针对ViewPager2的 offscreenPageLimit 特性和 预加载 展开讲解,包括Adapter的状态和Fragment的生命周期

ViewPager顽疾

为什么说 offscreenPageLimitViewPager 上十分不友好,可能是因为 offscreenPageLimit 最小值只能是1吧;

ViewPager2重大更新,支持offscreenPageLimit

上面是ViewPager默认情况下的加载示意图,当切换到当前页面时,会默认预加载左右两侧的布局到 ViewPager 中,尽管两侧的View并不可见的,我们称这种情况叫 预加载 ;由于 ViewPageroffscreenPageLimit 设置了限制,页面的预加载是不可避免;

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)
destroyItem(iewGroup container, int position, Object object)
isViewFromObject(View view, Object object)
setPrimaryItem(ViewGroup container, int position, Object object)
getCount()

先说 setPrimaryItem(ViewGroup container, int position, Object object) ,该方法表示当前页面正在显示主要 Item ,何为主要 Item ?如果预加载的ItemView已经划入屏幕,当前的 PrimaryItem 依然不会改变,除非新的ItemView完全划入屏幕,且滑动已经停止才会判断;

ViewPager2重大更新,支持offscreenPageLimit

由于 ViewPager 不可避免的进行布局预加载,造成 PagerAdapter 必须提前调用 instantiateItem(ViewGroup container, int position) 方法, instantiateItem() 是创建ItemView的唯一入口方法,所以 PagerAdapter 的实现类 FragmentPagerAdapterFragmentStatePagerAdapter 必须抓住该方法进行 Fragment 对象的创建;

ViewPager2重大更新,支持offscreenPageLimit

碰巧的是, FragmentPagerAdapterFragmentStatePagerAdapter 一股脑的在 instantiateItem() 中进行创建且进行 addattach 操作,并没有在 setPrimaryItem() 方法中对 Fragment 进行操作;

因此,预加载会导致不可见的 Fragment 一股脑的调用 onCreateonCreateViewonResume 等方法,用户只能通过 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
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);
    }
}
复制代码
  • 设置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 本质上是重写 LinearLayoutManagercalculateExtraLayoutSpace 方法,该方法是最新的 recyclerView 包加入的功能;

calculateExtraLayoutSpace 方法定义了布局额外的空间,何为布局额外的控件,默认布局的空间等于RecyclerView的空间,定义这个意在可以放大布局空间,其中参数 extraLayoutSpace 是一个长度为2的int数组,第一条数据接受左边/上边的额外空间,第二条数据接受右边/下边的额外空间,想知道更细节的逻辑都在LinearLayoutManager里;

综上代码, OffscreenPageLimit 可能就是放大了 LinearLayoutManager 的布局空间,我们下面看运行效果;

布局对比

为了对比两者加载布局的效果,我准备了LinearLayout同时展示ViewPager和ViewPager2,设置相同的Item布局和数据源,然后用Android布局分析 工具 抓取两者的布局结构,代码比较简单,就不贴出来了;

默认 offscreenPageLimit

ViewPager2重大更新,支持offscreenPageLimit

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

设置 offscreenPageLimit=2

ViewPager2重大更新,支持offscreenPageLimit

分析运行结果,在设置相同的 offscreenPageLimit 时,两者都会预布局左右(上下)两者的 offscreenPageLimit 个ItemView;

从对比结果上来看, ViewPager2offscreenPageLimitViewPager 运行结果一样,但是 ViewPager2 最小 offscreenPageLimit 可以设置为0;

ViewPager2预加载和缓存

ViewPager2预加载RecyclerView 的预加载,代码在 RecyclerViewGapWorker 中,这个知识可能有些同学不是很了解,推荐先看这篇博客 medium.com/google-deve…

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

ViewPager2重大更新,支持offscreenPageLimit

如何关闭预加载?

((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 :

ViewPager2Adapter 本质上是 RecyclerView.Adapter ,下面列举常用方法:

onCreateViewHolder(ViewGroup parent, int viewType)
onBindViewHolder(VH holder, int position)
onViewRecycled(VH holder)
onViewAttachedToWindow(VH holder)
onViewDetachedFromWindow(VH holder)
getItemCount()

下面主要针对 ItemView 的创建来说,暂不讨论回收的情况;

onBindViewHolder
onViewAttachedToWindow
onViewDetachedFromWindow

小结: 预加载缓存Adapter 层面没有区别,都会调用 onBindViewHolder 方法; 预加载离屏加载Adapter 层面有本质的区别, 离屏加载 的View会调用 onViewAttachedToWindow

ViewPager2对Fragment支持

目前, ViewPager2Fragment 的支持只能使用 FragmentStateAdapter ,使用起来也是非常简单:

ViewPager2重大更新,支持offscreenPageLimit

默认情况下, ViewPager2 是开启 预加载 关闭 离屏加载 的,这种情况下,切换页面对Fragment生命周如何?

ViewPager2重大更新,支持offscreenPageLimit

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

问题二:开启离屏加载对 Fragment 的影响: 设置offscreenPageLimit=1时:

ViewPager2重大更新,支持offscreenPageLimit

打印结果解读:

备注:log日志下标是从2开始的,标注的页码是从1开始,请自行矫正;

  • 默认情况下, ViewPager2 会缓存两条数据,所以滑动到第4页,第1页的Fragment才开始移除,这可以理解;
  • 设置offscreenPageLimit=1时, ViewPager2 在第1页会加载两条数据,这可以理解,会把下一页View提前加载进来;以后每滑一页,会加载下一页数组,直到第5页,会移除第1页的 Fragment ;第6页会移除第2页的 Fragment

如何理解 offscreenPageLimitFragment 的影响,假设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_PARENTFrameLayout ,注意这里并不像 PagerAdapterFragmentrootView

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() 方法调用 onViewAttachedToWindowFragmenthodler 绑定;

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小结:

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

总结

这一次 ViewPager2 更新,官方貌似要发力替换掉 ViewPager 了,无论是高效的复用还是自带懒加载的Adapter来看,都要比 ViewPager 要强大,如果想用建议尝试升级,但是谨慎使用 Fragment + offscreenPageLimit>0 的组合。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

动手玩转Scratch2.0编程

动手玩转Scratch2.0编程

马吉德·马吉 (Majed Marji) / 电子工业出版社 / 2015-10-1 / CNY 69.00

Scratch 是可视化的编程语言,其丰富的学习环境适合所有年龄阶段的人。利用它可以制作交互式程序、富媒体项目,包括动画故事、读书报告、科学实验、游戏和模拟程序等。《动手玩转Scratch2.0编程—STEAM创新教育指南》的目标是将Scratch 作为工具,教会读者最基本的编程概念,同时揭示Scratch 在教学上的强大能力。 《动手玩转Scratch2.0编程—STEAM创新教育指南》共......一起来看看 《动手玩转Scratch2.0编程》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具