内容简介:最近为什么说上面是ViewPager默认情况下的加载示意图,当切换到当前页面时,会默认预加载左右两侧的布局到
最近 ViewPager2
发布了 1.0.0-alpha04
版本,新增 offscreenPageLimit
功能,该功能在 ViewPager
上并不友好,现在官方将此功能延续下来,这回是骡子是马呢?赶紧拉出来溜溜;
阅读指南:
- 基于ViewPager2
1.0.0-alpha04
版本讲解,由于正式版还未发布,如有功能变动还请读者指点 - 本文主要针对ViewPager2的
offscreenPageLimit
特性和预加载
展开讲解,包括Adapter的状态和Fragment的生命周期
ViewPager顽疾
为什么说 offscreenPageLimit
在 ViewPager
上十分不友好,可能是因为 offscreenPageLimit
最小值只能是1吧;
上面是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懒加载前因后果
先说 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完全划入屏幕,且滑动已经停止才会判断;
由于 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
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
本质上是重写 LinearLayoutManager
的 calculateExtraLayoutSpace
方法,该方法是最新的 recyclerView
包加入的功能;
calculateExtraLayoutSpace
方法定义了布局额外的空间,何为布局额外的控件,默认布局的空间等于RecyclerView的空间,定义这个意在可以放大布局空间,其中参数 extraLayoutSpace
是一个长度为2的int数组,第一条数据接受左边/上边的额外空间,第二条数据接受右边/下边的额外空间,想知道更细节的逻辑都在LinearLayoutManager里;
综上代码, 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
中,这个知识可能有些同学不是很了解,推荐先看这篇博客 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
:
ViewPager2
的 Adapter
本质上是 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支持
目前, 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绑定 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()
方法调用 onViewAttachedToWindow
将 Fragment
和 hodler
绑定;
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小结:
- 目前
ViewPager2
对Fragment
支持只能用FragmentStateAdapter
,FragmentStateAdapter
在遇到预加载
时,只会创建Fragment
对象,不会把Fragment
真正的加入到布局中,所以自带懒加载效果; -
FragmentStateAdapter
不会一直保留Fragment
实例,回收的ItemView
也会移除Fragment
,所以得做好Fragment`重建后恢复数据的准备; -
FragmentStateAdapter
在遇到offscreenPageLimit>0时,处理离屏Fragment
和可见Fragment
没有什么区别,所以无法通过setUserVisibleHint
判断显示与否,这一点知得注意;
总结
这一次 ViewPager2
更新,官方貌似要发力替换掉 ViewPager
了,无论是高效的复用还是自带懒加载的Adapter来看,都要比 ViewPager
要强大,如果想用建议尝试升级,但是谨慎使用 Fragment
+ offscreenPageLimit>0
的组合。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Vardump 更新,支持打印各种 Java 数据结构,支持嵌套
- Vardump 更新,支持打印各种 Java 数据结构,支持嵌套
- Java 8 语言功能支持更新
- Microsoft Edge 更新,提供暗黑模式支持
- OSCHINA APP 更新,文章支持代码高亮
- Eclipse 4.9 发布更新,支持 Java 11
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
动手玩转Scratch2.0编程
马吉德·马吉 (Majed Marji) / 电子工业出版社 / 2015-10-1 / CNY 69.00
Scratch 是可视化的编程语言,其丰富的学习环境适合所有年龄阶段的人。利用它可以制作交互式程序、富媒体项目,包括动画故事、读书报告、科学实验、游戏和模拟程序等。《动手玩转Scratch2.0编程—STEAM创新教育指南》的目标是将Scratch 作为工具,教会读者最基本的编程概念,同时揭示Scratch 在教学上的强大能力。 《动手玩转Scratch2.0编程—STEAM创新教育指南》共......一起来看看 《动手玩转Scratch2.0编程》 这本书的介绍吧!