资讯专栏INFORMATION COLUMN

ViewPager2重大更新,支持offscreenPageLimit

番茄西红柿 / 3631人阅读

摘要:前言最近发布了版本,新增功能,该功能在上并不友好,现在官方将此功能延续下来,这回是骡子是马呢赶紧拉出来溜溜阅读指南内容基于版本讲解,由于正式版还未发布,如有功能变动有劳看官指出内容重点介绍的特性和预加载机制,另外包括的状态和的生命周

前言

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

阅读指南:

内容基于ViewPager21.0.0-alpha04版本讲解,由于正式版还未发布,如有功能变动有劳看官指出

内容重点:介绍ViewPager2的offscreenPageLimit特性和预加载机制,另外包括Adapter的状态和Fragment的生命周期等内容

ViewPager顽疾

顽疾是什么鬼,没有这么严重吧。ViewPager有两个毛病:不能关闭预加载更新Adapter不生效,所以开头我为什么说offscreenPageLimitViewPager上十分不友好;本质上是因为offscreenPageLimit不能设置成0(设置成0就是想象中的关闭预加载);

上面是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)初始化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的实现类FragmentPagerAdapterFragmentStatePagerAdapter必须抓住该方法进行Fragment对象的创建;

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

因此,预加载会导致不可见的Fragment一股脑的调用onCreateonCreateViewonResume等方法,用户只能通过Fragment.setUserVisibleHint()方法进行识别;

大多数的懒加载都是对Fragment做手脚,结合生命周期方法和setUserVisibleHint状态,控制数据延迟加载,而布局只能提前进入;

ViewPager2基本使用

build.gradle引入

implementation androidx.viewpager2:viewpager2:1.0.0-alpha04

布局文件添加

"@+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() {
        @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数组,第一条数据接受左边/上边的额外空间,第二条数据接受右边/下边的额外空间,故上诉代码是表明左右/上下各扩大offscreenSpace

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

布局对比

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

默认offscreenPageLimit

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

设置offscreenPageLimit=2

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

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

ViewPager2预加载和缓存

ViewPager2预加载RecyclerView的预加载,代码在RecyclerViewGapWorker中,这个知识可能有些同学不是很了解,推荐先看这篇博客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上;

提前加载对Adapter影响

所谓的提前加载,是指当前position不可见但加载了布局,包括上面说的预加载离屏加载,下面先介绍一下Adapter:

ViewPager2Adapter本质上是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支持

目前,ViewPager2Fragment的支持只能使用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

如何理解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判断显示与否,这一点知得注意;

ViewPager懒加载请注意

新版的Fragment中(Version 1.1.0-alpha07),该方法setUserVisibleHint已经过时,由FragmentTransactionsetMaxLifecycle替代,新版本的FragmentPagerAdapter可以设置直接调用生命周期,这代表ViewPager+Fragment懒加载有更好的解决方案,请注意

最后 ViewPager2更多优点

由于本章篇幅有点,没有对ViewPager2进行的全面介绍,不代表ViewPager就仅此而已,就当前版本来看,ViewPager2的优点或者特有的功能如下:

支持RecyclerView级别的复用

支持预加载和离屏加载(本章介绍)

支持动态更新Adapter(ViewPager大坑之一)

支持模拟拖拽

支持竖直方向滑动

支持页面滑动状态监听和页面变换(延续了ViewPager的功能)

只想到这么多了

总结

这一次ViewPager2更新,官方貌似要发力替换ViewPager了,无论是它高效的复用还是自带懒加载,亦或是更新有效的Adapter,都要比ViewPager强大,如果看官老爷们想尝试升级,在下十分赞赏,但从当前版本来看,请谨慎使用Fragment+offscreenPageLimit>0组合的情况。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/7041.html

相关文章

  • ViewPager2避坑系列】瞬间暴增数个Fragment

    摘要:前言最近我在关注的使用,期间一直基于官方的调试,今天遇到一个奇葩的问题,捉摸了半天最终找到原因,原来是中布局的问题,事后感觉有必要分享一下这个过程,一来可以巩固测量的知识,二来希望大家能避开这个坑阅读指南代码基于,看官老爷最好能下载前言 最近我在关注ViewPager2的使用,期间一直基于官方的Demo调试android-viewpager2,今天遇到一个奇葩的问题,捉摸了半天最终找到原因,...

    番茄西红柿 评论0 收藏0
  • Bootstrap 4重大更新,亮点解读

    摘要:重大更新亮点解读月日对来说是个特别的日子不仅是项目四周年纪念日,也是经过了一年密集开发之后发布内测版的日子。是一次重大更新,几乎涉及每行代码。 Bootstrap 4重大更新、亮点解读 8月19日对Bootstrap来说是个特别的日子——不仅是项目四周年纪念日,也是经过了一年密集开发之后发布Bootstrap 4内测版的日子。Bootstrap 4是一次重大更新,几乎涉及每行代码。 新...

    golden_hamster 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<