前言
最近ViewPager2
发布了1.0.0-alpha04
版本,新增offscreenPageLimit
功能,该功能在ViewPager
上并不友好,现在官方将此功能延续下来,这回是骡子是马呢?赶紧拉出来溜溜;
阅读指南:
- 内容基于ViewPager2
1.0.0-alpha04
版本讲解,由于正式版还未发布,如有功能变动有劳看官指出 - 内容重点:介绍ViewPager2的
offscreenPageLimit
特性和预加载
机制,另外包括Adapter的状态和Fragment的生命周期等内容
ViewPager顽疾
顽疾是什么鬼,没有这么严重吧。ViewPager
有两个毛病:不能关闭预加载
和更新Adapter不生效
,所以开头我为什么说offscreenPageLimit
在ViewPager
上十分不友好;本质上是因为offscreenPageLimit
不能设置成0(设置成0就是想象中的关闭预加载);
上面是ViewPager默认情况下的加载示意图,当切换到当前页面时,会默认预加载左右两侧的布局到ViewPager
中,尽管两侧的View并不可见的,我们称这种情况叫预加载
;由于ViewPager
对offscreenPageLimit
设置了限制,页面的预加载是不可避免;
ViewPager1
2
3
4
5
6
7
8
9
10
11
12
13private 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,返回需要添加ItemViewdestroyItem(iewGroup container, int position, Object object)
销毁ItemView,移除指定的ItemViewisViewFromObject(View view, Object object)
View和Object是否对应setPrimaryItem(ViewGroup container, int position, Object object)
当前页面的主ItemgetCount()
获取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引入
1
implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha04'
布局文件添加
1
2
3
4
5<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />设置ViewHolder+Adapter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25ViewPager2 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
1
2
3
4
5
6
7
8
9
10
11
12viewPager.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
的预加载存的的意义似乎是一样的;
ViewPager21
2
3
4
5
6
7
8
9
10
11public 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.LinearLayoutManagerImpl1
2
3
4
5
6
7
8
9
10
11
12
13
14@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
的布局空间,我们下面看运行效果;
布局对比
为了对比两者加载布局的效果,我准备了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
时,可能会预加载一条数据;下面是预加载的示意图:
如何关闭预加载?1
((RecyclerView)viewPager.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);
预加载的开关在LayoutManager
上,只需要获取LayoutManager
并调用setItemPrefetchEnabled()
即可控制开关;
ViewPager2
默认会缓存2条ItemView
,而且在最新的RecyclerView
中可以自定义缓存Item的个数;
RecyclerView1
2
3public void setItemViewCacheSize(int size) {
mRecycler.setViewCacheSize(size);
}
小结:预加载
和缓存
在View
层面没有本质的区别,都是已经准备了布局,但是没有加载到parent上;预加载
和离屏加载
在View
层面有本质的区别,离屏加载
的View已经添加到parent上;
提前加载对Adapter影响
所谓的提前加载,是指当前position
不可见但加载了布局,包括上面说的预加载
和离屏加载
,下面先介绍一下Adapter
:
ViewPager2
的Adapter
本质上是RecyclerView.Adapter
,下面列举常用方法:
onCreateViewHolder(ViewGroup parent, int viewType)
创建ViewHolderonBindViewHolder(VH holder, int position)
绑定ViewHolderonViewRecycled(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()方法1
2
3
4
5
6
7
8
9
10
11
12public 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()
1 | public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) { |
onBindViewHolder()
首先会获取当前position对应的Fragment
,这意味着预加载的Fragment
对象会提前创建;- 如果当前的holder.itemView已经添加到屏幕且已经布局且parent不等于空,就会将Fragment绑定到ViewHodler;
- 每次调用都会gc一次,主要的避免用户修改数据源造成垃圾对象;
onViewAttachedToWindow()1
2
3
4public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
placeFragmentInViewHolder(holder);
gcFragments();
}
onViewAttachedToWindow()
方法调用onViewAttachedToWindow
将Fragment
和hodler
绑定;
onViewRecycled()1
2
3
4
5
6
7
8public 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
移除;
核心添加操作:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 //将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
判断显示与否,这一点知得注意;
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
组合的情况。