ViewPager2已经出来很长一段时间了,但之前一直都是alpha版本,几次版本迭代中,内容细节变化也挺多,前阵子第一个正式版发布,不巧新冠肺炎爆发,在家里索性把之前的预研Demo整理整理,梳理下内容点写一篇博客,也算把预研工作正式收个尾。
首先先感谢一个我确实记不得的大兄嘚,预研的demo的前身,来自于GitHub,是我很久之前看alpha版本的使用时下载的,只下了一个zip,着实找不到源头了。年前开始预研的时候,我正好发现电脑里面有一个项目,就懒得新建,直接改了一通。
先贴上本文的 demo地址 (https://github.com/leobert-lan/ViewPager2-Demo/tree/master)
本文的篇幅会比较长,先给一个大致的内容梗概:
最关键是我写写停停,有时候一篇文章拖个把月,不先放个梗概我自己都会忘了要写啥。
按照以往用ViewPager的经验,我们会使用到三个东西:
而使用ViewPager2也是类似的,我们需要一个ViewPager2实例,一个相应的适配器Adapter(RecyclerView.Adapter的子类),子视图和相应的RecyclerView.ViewHolder子类实例。
我们可能对ViewPager2有点或多或少的了解,他是通过RecyclerView做的功能实现,这里先不展开。
package com.example.viewpager2demo;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
/**
* leobert
*/
public class ViewPagerAdapter extends RecyclerView.Adapter {
private List colors = new ArrayList<>();
{
colors.add(android.R.color.black);
colors.add(android.R.color.holo_purple);
colors.add(android.R.color.holo_blue_dark);
colors.add(android.R.color.holo_green_light);
}
@NonNull
@Override
public ViewPagerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewPagerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_page, parent,false));
}
@Override
public void onBindViewHolder(@NonNull ViewPagerViewHolder holder, int position) {
holder.mTvTitle.setText("item " + position);
holder.mContainer.setBackgroundResource(colors.get(position));
}
@Override
public int getItemCount() {
return colors.size();
}
class ViewPagerViewHolder extends RecyclerView.ViewHolder {
TextView mTvTitle;
RelativeLayout mContainer;
public ViewPagerViewHolder(@NonNull View itemView) {
super(itemView);
mContainer = itemView.findViewById(R.id.container);
mTvTitle = itemView.findViewById(R.id.tvTitle);
}
}
}
布局都是比较简单的内容,这里不贴了。
使用的时候:
setContentView(R.layout.activity_horizontal_scrolling);
ViewPager2 viewPager2 = findViewById(R.id.viewpager2);
ViewPagerAdapter viewPagerAdapter = new ViewPagerAdapter();
viewPager2.setAdapter(viewPagerAdapter);
使用也是很简单,具体可以看demo中的HorizontalScrolling
那么纵向滚动呢?
setContentView(R.layout.activity_horizontal_scrolling);
ViewPager2 viewPager2 = findViewById(R.id.viewpager2);
ViewPagerAdapter viewPagerAdapter = new ViewPagerAdapter();
viewPager2.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
viewPager2.setAdapter(viewPagerAdapter);
只需要设置下方向即可,ViewPager的默认方式是横向的,这点我们可以在源码中找到:
androidx.viewpager2.widget.ViewPager2#initialize方法中,实例化了LayoutManager,并调用了androidx.viewpager2.widget.ViewPager2#setOrientation(android.content.Context, android.util.AttributeSet)
private void setOrientation(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewPager2);
if (Build.VERSION.SDK_INT >= 29) {
saveAttributeDataForStyleable(context, R.styleable.ViewPager2, attrs, a, 0, 0);
}
try {
setOrientation(
a.getInt(R.styleable.ViewPager2_android_orientation, ORIENTATION_HORIZONTAL));
} finally {
a.recycle();
}
}
可以看到默认方向就是横向的。
这里我们简单小结一下:
ViewPager2可以像RecyclerView一样使用,并且可以配滑动方向了。我们只需要一个ViewPager2、一套ViewHolder,一个Adapter,就可以开始使用了。
结合我们日常工作实际,现在单独使用VP (ViewPager,如无特殊必要,下文都会使用VP来指代滑动控件,语义上如无必要区分ViewPager和ViewPager2,均以VP指代,否则以VP2指代ViewPager2)去呈现View的场景是比较少的,往往是用来显示图片,更多的业务往往会结合生命周期感知、以及需要实现业务“组件化”(这里的组件化指的是其环境相对独立,便于场景移植快速使用),而实际案例中都是使用Fragment去作为业务的承载。
我们知道,VP结合Fragment使用是一个很常见的套路,那么VP2中是否可以呢?当然是可以的,否则不支持向后兼容性迭代,这个VP2就是个笑话了。
言归正传、结合我们使用VP+Fragment的经验,我们会需要VP、FragmentPagerAdapter或者FragmentStatePagerAdapter和一系列的Fragment,限于内容主题和篇幅就放Demo代码了。ok,按照老经验,我们推断这里也会使用到VP2,一个特定的Adapter,按照兼容原则,Fragment应该没有特殊限制。
经过一番阅读,我们知道了Google的工程师们给我们提供的是:
androidx.viewpager2.adapter.FragmentStateAdapter
我们“特定的Adapter”就是这玩意了,先上代码,回头再看细节:
class ViewPagerFragmentStateAdapter extends FragmentStateAdapter {
public ViewPagerFragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
public ViewPagerFragmentStateAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) {
super(fragmentManager, lifecycle);
}
@NonNull
@Override
public Fragment createFragment(int position) {
if (position == 0)
return RvFragment.Companion.newInstance();
else
return PageFragment.newInstance(colors, position - 1);
}
@Override
public int getItemCount() {
return colors.size() + 1;
}
}
这里我摘了一段Demo中的内容,抛去里面的方法具体实现,使用的时候,构造器真正有用的是第二种,给FragmentActivity或者Fragment(这里没有体现),都是取出对应的FragmentManager和Lifecycle的,我们可以扫一眼源码:
/**
* @param fragmentActivity if the {@link ViewPager2} lives directly in a
* {@link FragmentActivity} subclass.
*
* @see FragmentStateAdapter#FragmentStateAdapter(Fragment)
* @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle)
*/
public FragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) {
this(fragmentActivity.getSupportFragmentManager(), fragmentActivity.getLifecycle());
}
/**
* @param fragment if the {@link ViewPager2} lives directly in a {@link Fragment} subclass.
*
* @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity)
* @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle)
*/
public FragmentStateAdapter(@NonNull Fragment fragment) {
this(fragment.getChildFragmentManager(), fragment.getLifecycle());
}
/**
* @param fragmentManager of {@link ViewPager2}'s host
* @param lifecycle of {@link ViewPager2}'s host
*
* @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity)
* @see FragmentStateAdapter#FragmentStateAdapter(Fragment)
*/
public FragmentStateAdapter(@NonNull FragmentManager fragmentManager,
@NonNull Lifecycle lifecycle) {
mFragmentManager = fragmentManager;
mLifecycle = lifecycle;
super.setHasStableIds(true);
}
必须要实现的两个抽象方法是:
androidx.viewpager2.adapter.FragmentStateAdapter#createFragment
/**
* Provide a new Fragment associated with the specified position.
*
* The adapter will be responsible for the Fragment lifecycle:
*
* - The Fragment will be used to display an item.
* - The Fragment will be destroyed when it gets too far from the viewport, and its state
* will be saved. When the item is close to the viewport again, a new Fragment will be
* requested, and a previously saved state will be used to initialize it.
*
* @see ViewPager2#setOffscreenPageLimit
*/
public abstract @NonNull Fragment createFragment(int position);
和:androidx.recyclerview.widget.RecyclerView.Adapter#getItemCount
/**
* Returns the total number of items in the data set held by the adapter.
*
* @return The total number of items in this adapter.
*/
public abstract int getItemCount();
考虑到FragmentStateAdapter的代码确实有点长,而这里还没有开始做代码分析,先贴上Google给的官方文档
/** * Similar in behavior to {@link FragmentStatePagerAdapter} *
* Lifecycle within {@link RecyclerView}: *
*
*/- {@link RecyclerView.ViewHolder} initially an empty {@link FrameLayout}, serves as a * re-usable container for a {@link Fragment} in later stages. *
- {@link RecyclerView.Adapter#onBindViewHolder} we ask for a {@link Fragment} for the * position. If we already have the fragment, or have previously saved its state, we use those. *
- {@link RecyclerView.Adapter#onAttachedToWindow} we attach the {@link Fragment} to a * container. *
- {@link RecyclerView.Adapter#onViewRecycled} we remove, save state, destroy the * {@link Fragment}. *
文档中提到,这个adapter的行为和FragmentStatePagerAdapter 是类似的,(言下之意是会处理saveInstanceState以及restore),他的生命周期会在RecyclerView之内,会先用ViewHolder创建一个空布局,他会被后续创建出来的Fragment作为容器,而且会被复用;在onBindViewHolder时,会按照位置请求需要的Fragment,如果这个所需的Fragment实例已经存在或者之前存储其状态,会直接使用这些内容;Fragment在adapter的onAttachedToWindow阶段附着到之前提到的容器(ViewHolder中的FrameLayout);在adapter的onViewRecyclered阶段,会去处理Fragment的“remove”,“saveInstanceState”,“destroy”。
具体代码参考Demo中的:
com.example.viewpager2demo.FragmentStateAdapterActivity
Log 这里就不贴了,下面我们会带着问题看生命周期
这一章节是一个引申章节,如果暂时不想将注意力移开的话,可以跳过本章节。
我们先贴几张老图:
Fragment生命周期 | Fragment生命周期和Activity生命周期的关系 |
这里列出了Fragment的生命周期变化以及和Activity生命周期的关系,都是老知识了,不展开;
我们也知道JetPack系列中给了一个Lifecycle,这里我们稍微扯开一下,为什么要提供这个东西?
假设没有这个东西,我们需要在Fragment(或者Activity中)相应的生命周期方法回调中进行业务编码,让Fragment(或者Activity)持有需要感知生命周期的对象引用,并在生命周期回调中进行相关方法调用,看起来好像没啥太大的问题,就是没有顶层设计,带来:缺乏统一的行为模式问题,如果要自己实现的话,是一个“有规模”的工作量,并且项目中的各种基类都需要修改,如果有三方库中的基类,还需要一番折腾。
有了这个东西,我们可以遵循顶层设计,便捷的完成生命周期感知。
OK,到这里我们都是在讲题外话,其实结合我们的题外知识,我们知道Lifecycle是有一套玩法的,尤其是在Fragment中,需要结合:
androidx.fragment.app.FragmentTransaction#setMaxLifecycle
我们需要做的是试验一下,FragmentStateAdapter中是否为我们正确的处理了这件事情。
我们打印一下log:
mOffscreenPageLimit是default情况下:
2020-03-30 20:22:59.272 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: RvFragment ON_CREATE
2020-03-30 20:22:59.272 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: RvFragment ON_START
2020-03-30 20:22:59.273 23446-23446/com.example.viewpager2demo E/lmsg: onResume:RvFragment
2020-03-30 20:22:59.273 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: RvFragment ON_RESUME
2020-03-30 20:23:07.304 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 0 ON_CREATE
2020-03-30 20:23:07.304 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 0 ON_START
2020-03-30 20:23:07.738 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: RvFragment ON_PAUSE
2020-03-30 20:23:07.738 23446-23446/com.example.viewpager2demo E/lmsg: onPause:RvFragment
2020-03-30 20:23:07.738 23446-23446/com.example.viewpager2demo E/lmsg: onResume:PageFragment 0
2020-03-30 20:23:07.739 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 0 ON_RESUME
2020-03-30 20:23:09.083 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 1 ON_CREATE
2020-03-30 20:23:09.083 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 1 ON_START
2020-03-30 20:23:09.532 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 0 ON_PAUSE
2020-03-30 20:23:09.533 23446-23446/com.example.viewpager2demo E/lmsg: onPause:PageFragment 0
2020-03-30 20:23:09.533 23446-23446/com.example.viewpager2demo E/lmsg: onResume:PageFragment 1
2020-03-30 20:23:09.534 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 1 ON_RESUME
2020-03-30 20:23:10.426 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 2 ON_CREATE
2020-03-30 20:23:10.427 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 2 ON_START
2020-03-30 20:23:10.859 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: RvFragment ON_STOP
2020-03-30 20:23:10.859 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: RvFragment ON_DESTROY
2020-03-30 20:23:10.863 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 1 ON_PAUSE
2020-03-30 20:23:10.864 23446-23446/com.example.viewpager2demo E/lmsg: onPause:PageFragment 1
2020-03-30 20:23:10.864 23446-23446/com.example.viewpager2demo E/lmsg: onResume:PageFragment 2
2020-03-30 20:23:10.864 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 2 ON_RESUME
2020-03-30 20:23:12.051 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 3 ON_CREATE
2020-03-30 20:23:12.051 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 3 ON_START
2020-03-30 20:23:12.454 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 2 ON_PAUSE
2020-03-30 20:23:12.454 23446-23446/com.example.viewpager2demo E/lmsg: onPause:PageFragment 2
2020-03-30 20:23:12.454 23446-23446/com.example.viewpager2demo E/lmsg: onResume:PageFragment 3
2020-03-30 20:23:12.455 23446-23446/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 3 ON_RESUME
我们把mOffscreenPageLimit改为1(懒加载中我们还会提到这个)
2020-03-30 20:37:19.762 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: RvFragment ON_CREATE
2020-03-30 20:37:19.762 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: RvFragment ON_START
2020-03-30 20:37:19.762 24759-24759/com.example.viewpager2demo E/lmsg: onResume:RvFragment
2020-03-30 20:37:19.762 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: RvFragment ON_RESUME
2020-03-30 20:37:19.795 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 0 ON_CREATE
2020-03-30 20:37:19.795 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 0 ON_START
第一页已经显示
切换到第二页
2020-03-30 20:37:46.293 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 1 ON_CREATE
2020-03-30 20:37:46.294 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 1 ON_START
2020-03-30 20:37:46.672 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: RvFragment ON_PAUSE
2020-03-30 20:37:46.674 24759-24759/com.example.viewpager2demo E/lmsg: onPause:RvFragment
2020-03-30 20:37:46.675 24759-24759/com.example.viewpager2demo E/lmsg: onResume:PageFragment 0
2020-03-30 20:37:46.676 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 0 ON_RESUME
切换到第三页
2020-03-30 20:37:54.440 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 2 ON_CREATE
2020-03-30 20:37:54.440 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 2 ON_START
2020-03-30 20:37:54.826 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 0 ON_PAUSE
2020-03-30 20:37:54.827 24759-24759/com.example.viewpager2demo E/lmsg: onPause:PageFragment 0
2020-03-30 20:37:54.827 24759-24759/com.example.viewpager2demo E/lmsg: onResume:PageFragment 1
2020-03-30 20:37:54.827 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 1 ON_RESUME
切换到第四页
2020-03-30 20:38:05.132 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 3 ON_CREATE
2020-03-30 20:38:05.132 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 3 ON_START
2020-03-30 20:38:05.517 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 1 ON_PAUSE
2020-03-30 20:38:05.518 24759-24759/com.example.viewpager2demo E/lmsg: onPause:PageFragment 1
2020-03-30 20:38:05.518 24759-24759/com.example.viewpager2demo E/lmsg: onResume:PageFragment 2
2020-03-30 20:38:05.518 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 2 ON_RESUME
切换到第五页
2020-03-30 20:38:16.223 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 2 ON_PAUSE
2020-03-30 20:38:16.224 24759-24759/com.example.viewpager2demo E/lmsg: onPause:PageFragment 2
2020-03-30 20:38:16.224 24759-24759/com.example.viewpager2demo E/lmsg: onResume:PageFragment 3
2020-03-30 20:38:16.225 24759-24759/com.example.viewpager2demo D/lmsg: onStateChanged: PageFragment 3 ON_RESUME
我们发现onDestroy不见了。我们把页面内容调多一点,log先不放了,有点长,可以跑一下Demo,发现onStop和onDestroy又出现了,只是比我们想像的有点远。
按照页面:0,1,2,3,4,5,6,7;我们推测的,顺着页面滑动,到2的时候0应该走向销毁。但实际情况不是如此,而是到了4的时候,0才销毁。按照我的性格,这个地方不扒一下那blog还是别写了。
按照我们曾经阅读过的代码,我们从Fragment的onStop入手反向去找就能找到原因,(没读过的最好配合点资料把源码读一读,或者生死看淡)。我们会最终找到:
androidx.viewpager2.adapter.FragmentStateAdapter#removeFragment
private void removeFragment(long itemId) {
Fragment fragment = mFragments.get(itemId);
//中间代码略去
mFragmentManager.beginTransaction().remove(fragment).commitNow();
mFragments.remove(itemId);
}
这里大体给一下内容参考:
androidx.fragment.app.FragmentTransaction#remove
addOp(new Op(OP_REMOVE, fragment));
void executeOps() {
final int numOps = mOps.size();
for (int opNum = 0; opNum < numOps; opNum++) {
final Op op = mOps.get(opNum);
final Fragment f = op.mFragment;
if (f != null) {
f.setNextTransition(mTransition, mTransitionStyle);
}
switch (op.mCmd) {
case OP_ADD:
f.setNextAnim(op.mEnterAnim);
mManager.addFragment(f, false);
break;
case OP_REMOVE:
f.setNextAnim(op.mExitAnim);
mManager.removeFragment(f);
//...
}
if (!mReorderingAllowed && op.mCmd != OP_ADD && f != null) {
mManager.moveFragmentToExpectedState(f);
}
}
if (!mReorderingAllowed) {
// Added fragments are added at the end to comply with prior behavior.
mManager.moveToState(mManager.mCurState, true);
}
}
androidx.fragment.app.FragmentManagerImpl#removeFragment
androidx.fragment.app.FragmentManagerImpl#moveToState(int, boolean)
androidx.fragment.app.FragmentManagerImpl#moveFragmentToExpectedState 即:
void moveFragmentToExpectedState(Fragment f) {
if (f == null) {
return;
}
if (!mActive.containsKey(f.mWho)) {
if (DEBUG) {
Log.v(TAG, "Ignoring moving " + f + " to state " + mCurState
+ "since it is not added to " + this);
}
return;
}
int nextState = mCurState;
if (f.mRemoving) {
if (f.isInBackStack()) {
nextState = Math.min(nextState, Fragment.CREATED);
} else {
nextState = Math.min(nextState, Fragment.INITIALIZING);
}
}
moveToState(f, nextState, f.getNextTransition(), f.getNextTransitionStyle(), false);
//...
}
再顺着往下面,后面方法的代码着实太多,就不贴了
而这个方法被三处调用:
目前发现上述操作手法中,是onViewRecyclered带来的onStop (这时候我又回过头来看了下文档,上面也提到过,我们花了很多力气倒过来找到的结论,文档中一开始就说了,参见本文:结合Fragment的使用 小节中 FragmentStateAdapter文档)
@Override
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);
}
}
阅读时请注意:Adapter中的一个成员变量的设计意图
private final LongSparseArray mItemIdToViewHolder = new LongSparseArray<>();
另外我们知道AndroidX1.0.0(以及support28)中,对Fragment加了点料:
androidx.fragment.app.Fragment#getViewLifecycleOwner
androidx.fragment.app.Fragment#getViewLifecycleOwnerLiveData
同样可以测一下监测到的生命周期。
关于这两个方法和androidx.fragment.app.Fragment#getLifecycle之间的区别,可以参考下官方API文档以及:
5-common-mistakes-when-using-architecture-components
可能需要,另搜索到一份简书上的翻译,https://www.jianshu.com/p/c1ee77f8237f
我们就不再做展开,下次有机会写个LiveData和Lifecycle的文章(其实是我还没准备好内容)
又是一个很常见的需求了,现在只要和资讯、内容搭点边的APP,首页基本都会分页签,以及出现搜索栏等高权重的内容,而为了打造沉浸式体验,又会在滚动交互时,对tab、搜索栏等进行一定的处理。
OK,不往远了扯,直接给出结论,VP2和VP一样,支持配合CoordinatorLayout和NestedScroll机制。
Demo中可以关注一下
ViewPager2-Demo/app/src/main/res/layout/activity_nested_test2.xml
Demo中我们实习了一个更加变态的多层级联效果,有复杂需求的同学建议自己再折腾折腾,因为我还没有准备充足的内容,不在这里展开写NestedScroll机制
这又是一个很常见的需求了,我们都知道,如果是在VP中使用Fragment,往往会结合懒加载以及预加载机制,所谓懒加载就是还没看到的不加载,所谓预加载就是还没看到但是可能会看到的也加载。但我们真正需要的往往是:只去预加载n页,更远的页面不加载,这个n一般是1或者2,而且这种加载按照交互需求,至少是加载视图,也可以更进一步加载数据(我就是想说一下我们的习惯性用词总是不准确),下文都用懒加载一词指代这一系列的加载模式。
按照我们的经验,我们要处理懒加载,需要结合VP的setOffscreenPageLimit以及Fragment的getUserVisibleHint来处理
/**
* Set the number of pages that should be retained to either side of the
* current page in the view hierarchy in an idle state. Pages beyond this
* limit will be recreated from the adapter when needed.
*
* This is offered as an optimization. If you know in advance the number
* of pages you will need to support or have lazy-loading mechanisms in place
* on your pages, tweaking this setting can have benefits in perceived smoothness
* of paging animations and interaction. If you have a small number of pages (3-4)
* that you can keep active all at once, less time will be spent in layout for
* newly created view subtrees as the user pages back and forth.
*
* You should keep this limit low, especially if your pages have complex layouts.
* This setting defaults to 1.
*
* @param limit How many pages will be kept offscreen in an idle state.
*/
public void setOffscreenPageLimit(int limit) {
if (limit < DEFAULT_OFFSCREEN_PAGES) {
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();
}
}
/**
* @return The current value of the user-visible hint on this fragment.
* @see #setUserVisibleHint(boolean)
*
* @deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)}
* instead.
*/
@Deprecated
public boolean getUserVisibleHint() {
return mUserVisibleHint;
}
从androidx-viewpager-1.0.0,androidx-Fragment-1.1.0中的源码我们发现,screenOffsetLimit 至少是1,也就是说,老的VP会在机制上先去创建至少左右相邻的两页的“内容”实例,而如果我们需要先获取数据的话,可以在一个相对较早的生命周期中处理,如果我们需要等到视图对用户可见的时候,就要结合getUserVIsibleHint返回true;
当然,这些是VP使用时的一些内容,不是今天的主角,我们不往深了扒。还是来扒VP2.
那么VP2能从机制上支持预加载和懒加载吗?
我们可以找到源码:
//androidx.viewpager2.widget.ViewPager2#setOffscreenPageLimit
/**
* Set the number of pages that should be retained to either side of the currently visible
* page(s). Pages beyond this limit will be recreated from the adapter when needed. Set this to
* {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT} to use RecyclerView's caching strategy. The given value
* must either be larger than 0, or {@code #OFFSCREEN_PAGE_LIMIT_DEFAULT}.
*
* Pages within {@code limit} pages away from the current page are created and added to the
* view hierarchy, even though they are not visible on the screen. Pages outside this limit will
* be removed from the view hierarchy, but the {@code ViewHolder}s will be recycled as usual by
* {@link RecyclerView}.
*
* This is offered as an optimization. If you know in advance the number of pages you will
* need to support or have lazy-loading mechanisms in place on your pages, tweaking this setting
* can have benefits in perceived smoothness of paging animations and interaction. If you have a
* small number of pages (3-4) that you can keep active all at once, less time will be spent in
* layout for newly created view subtrees as the user pages back and forth.
*
* You should keep this limit low, especially if your pages have complex layouts. By default
* it is set to {@code OFFSCREEN_PAGE_LIMIT_DEFAULT}.
*
* @param limit How many pages will be kept offscreen on either side. Valid values are all
* values {@code >= 1} and {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT}
* @throws IllegalArgumentException If the given limit is invalid
* @see #getOffscreenPageLimit()
*/
public void setOffscreenPageLimit(@OffscreenPageLimit 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();
}
我们找到一个命名很类似的方法,值只接受-1和>0,用来设置远离当前位置的“内容”的保留数量,在距离当前位置不超过limit的页面,都会被创建并被放入视图树内,如果你的页面不多(3-4个)可以全保留,省的老是创建销毁,页面数量比较多的,适当保留,可以获得比较好的动画以及交互效果。
结合我们前面对生命周期的研究,我们会意识到,在VP2中,Fragment走到stop/destory要比我们想像中的慢。
这里呢,我只能很不负责任的先给一下个人意见:
考虑到不同的产品对于体验的需求不一致,这篇文章不考虑展开各种体验需求下如果做懒加载,按照我们对生命周期的研究,总能找到一个适合你的需求的时机,去做加载,但不管怎么弄,都需要各位去测试状态恢复的“脏数据”问题。或者去试试Google给的ViewModel。
在我司的APP中,我们针对一些场景,对已经请求过网络的数据做了缓存,这样,在Fragment被销毁恢复后,可以减少没必要的网络请求
我们前面提到,VP2是通过RecyclerView实现的,现在我们从源码一探究竟。
public final class ViewPager2 extends ViewGroup
VP2是继承ViewGroup的,那么推测是内部嵌入了一个子View是RecyclerView
public ViewPager2(@NonNull Context context) {
super(context);
initialize(context, null);
}
public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context, attrs);
}
@RequiresApi(21)
public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize(context, attrs);
}
private void initialize(Context context, AttributeSet attrs) {
mAccessibilityProvider = sFeatureEnhancedA11yEnabled
? new PageAwareAccessibilityProvider()
: new BasicAccessibilityProvider();
mRecyclerView = new RecyclerViewImpl(context);
mRecyclerView.setId(ViewCompat.generateViewId());
mRecyclerView.setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
mLayoutManager = new LinearLayoutManagerImpl(context);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);
setOrientation(context, attrs);
mRecyclerView.setLayoutParams(
new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
mRecyclerView.addOnChildAttachStateChangeListener(enforceChildFillListener());
// Create ScrollEventAdapter before attaching PagerSnapHelper to RecyclerView, because the
// attach process calls PagerSnapHelperImpl.findSnapView, which uses the mScrollEventAdapter
mScrollEventAdapter = new ScrollEventAdapter(this);
// Create FakeDrag before attaching PagerSnapHelper, same reason as above
mFakeDragger = new FakeDrag(this, mScrollEventAdapter, mRecyclerView);
mPagerSnapHelper = new PagerSnapHelperImpl();
mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
// Add mScrollEventAdapter after attaching mPagerSnapHelper to mRecyclerView, because we
// don't want to respond on the events sent out during the attach process
mRecyclerView.addOnScrollListener(mScrollEventAdapter);
mPageChangeEventDispatcher = new CompositeOnPageChangeCallback(3);
mScrollEventAdapter.setOnPageChangeCallback(mPageChangeEventDispatcher);
// Callback that updates mCurrentItem after swipes. Also triggered in other cases, but in
// all those cases mCurrentItem will only be overwritten with the same value.
final OnPageChangeCallback currentItemUpdater = new OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
if (mCurrentItem != position) {
mCurrentItem = position;
mAccessibilityProvider.onSetNewCurrentItem();
}
}
@Override
public void onPageScrollStateChanged(int newState) {
if (newState == SCROLL_STATE_IDLE) {
updateCurrentItem();
}
}
};
// Prevents focus from remaining on a no-longer visible page
final OnPageChangeCallback focusClearer = new OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
clearFocus();
if (hasFocus()) { // if clear focus did not succeed
mRecyclerView.requestFocus(View.FOCUS_FORWARD);
}
}
};
// Add currentItemUpdater before mExternalPageChangeCallbacks, because we need to update
// internal state first
mPageChangeEventDispatcher.addOnPageChangeCallback(currentItemUpdater);
mPageChangeEventDispatcher.addOnPageChangeCallback(focusClearer);
// Allow a11y to register its listeners after currentItemUpdater (so it has the
// right data). TODO: replace ordering comments with a test.
mAccessibilityProvider.onInitialize(mPageChangeEventDispatcher, mRecyclerView);
mPageChangeEventDispatcher.addOnPageChangeCallback(mExternalPageChangeCallbacks);
// Add mPageTransformerAdapter after mExternalPageChangeCallbacks, because page transform
// events must be fired after scroll events
mPageTransformerAdapter = new PageTransformerAdapter(mLayoutManager);
mPageChangeEventDispatcher.addOnPageChangeCallback(mPageTransformerAdapter);
attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
}
这里的RecyclerViewImpl是VP2中的一个内部类,继承RecyclerView。
/**
* Slightly modified RecyclerView to get ViewPager behavior in accessibility and to
* enable/disable user scrolling.
*/
private class RecyclerViewImpl extends RecyclerView {
RecyclerViewImpl(@NonNull Context context) {
super(context);
}
@RequiresApi(23)
@Override
public CharSequence getAccessibilityClassName() {
if (mAccessibilityProvider.handlesRvGetAccessibilityClassName()) {
return mAccessibilityProvider.onRvGetAccessibilityClassName();
}
return super.getAccessibilityClassName();
}
@Override
public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.setFromIndex(mCurrentItem);
event.setToIndex(mCurrentItem);
mAccessibilityProvider.onRvInitializeAccessibilityEvent(event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
return isUserInputEnabled() && super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return isUserInputEnabled() && super.onInterceptTouchEvent(ev);
}
}
覆写onTouchEvent和onInterceptTouchEvent以采用设置在VP2上的设置(是否接受用户输入,例如滑动)。覆写onInitializeAccessibilityEvent以添加一些信息。
LayoutManager是继承LinearLayoutManager,按照“页”的概念处理了一些计算,反正也不能自己定制。
private class LinearLayoutManagerImpl extends LinearLayoutManager {
LinearLayoutManagerImpl(Context context) {
super(context);
}
@Override
public boolean performAccessibilityAction(@NonNull RecyclerView.Recycler recycler,
@NonNull RecyclerView.State state, int action, @Nullable Bundle args) {
if (mAccessibilityProvider.handlesLmPerformAccessibilityAction(action)) {
return mAccessibilityProvider.onLmPerformAccessibilityAction(action);
}
return super.performAccessibilityAction(recycler, state, action, args);
}
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,
@NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(recycler, state, info);
mAccessibilityProvider.onLmInitializeAccessibilityNodeInfo(info);
}
@Override
protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
@NonNull int[] extraLayoutSpace) {
int pageLimit = getOffscreenPageLimit();
if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
// Only do custom prefetching of offscreen pages if requested
super.calculateExtraLayoutSpace(state, extraLayoutSpace);
return;
}
final int offscreenSpace = getPageSize() * pageLimit;
extraLayoutSpace[0] = offscreenSpace;
extraLayoutSpace[1] = offscreenSpace;
}
@Override
public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent,
@NonNull View child, @NonNull Rect rect, boolean immediate,
boolean focusedChildVisible) {
return false; // users should use setCurrentItem instead
}
}
而adapter自然是我们之前提到的adapter,在VP2 setAdapter时,一并调用RecyclerView的setAdapter
public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) {
final Adapter> currentAdapter = mRecyclerView.getAdapter();
mAccessibilityProvider.onDetachAdapter(currentAdapter);
unregisterCurrentItemDataSetTracker(currentAdapter);
mRecyclerView.setAdapter(adapter);
mCurrentItem = 0;
restorePendingState();
mAccessibilityProvider.onAttachAdapter(adapter);
registerCurrentItemDataSetTracker(adapter);
}
考虑到我们前面已经提到了不少代码细节,在脱离具体问题的情况下就不再展开了,毕竟大多都是RecyclerView的知识
到这里我们再看一下配合Fragment使用时,ViewHolder是咋玩的。
/**
* {@link ViewHolder} implementation for handling {@link Fragment}s. Used in
* {@link FragmentStateAdapter}.
*/
public final class FragmentViewHolder extends ViewHolder {
private FragmentViewHolder(@NonNull FrameLayout container) {
super(container);
}
@NonNull static FragmentViewHolder create(@NonNull 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);
}
@NonNull FrameLayout getContainer() {
return (FrameLayout) itemView;
}
}
代码也是很简单,ViewHolder中其实只创建一个FrameLayout,从视图结构上,Fragment的视图是嵌入到这个FrameLayout的,另外,结合我们前面提到的FragmentStateAdapter的代码,这个FrameLayout的id和ViewHolder的itemId会参与到信息“缓存”机制,用于Fragment的“嵌入”和“移除”,这里就不再展开了。
到这里,我们已经很清晰的知道VP2如何使用RecyclerView来显示内容的了,接下来看看如果实现“页”的。
我们之前可能已经接触过SDK中的一个类:androidx.recyclerview.widget.PagerSnapHelper
private class PagerSnapHelperImpl extends PagerSnapHelper {
PagerSnapHelperImpl() {
}
@Nullable
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
// When interrupting a smooth scroll with a fake drag, we stop RecyclerView's scroll
// animation, which fires a scroll state change to IDLE. PagerSnapHelper then kicks in
// to snap to a page, which we need to prevent here.
// Simplifying that case: during a fake drag, no snapping should occur.
return isFakeDragging() ? null : super.findSnapView(layoutManager);
}
}
VP2中也没弄啥特殊的,搞了一个FakeDrag,也不能定制,先不看它。
那么滑动按“页”就是正常的PagerSnapHelper,看起来也没啥分析的必要了(其实就是我犯懒了)
正常使用,虽然看起来没啥必要
这是一个难得的可以定制的内容,但是呢,也不是新内容,参考ViewPager中如何使用的即可
/**
* A PageTransformer is invoked whenever a visible/attached page is scrolled.
* This offers an opportunity for the application to apply a custom transformation
* to the page views using animation properties.
*/
public interface PageTransformer {
/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
* position of the pager. 0 is front and center. 1 is one full
* page position to the right, and -2 is two pages to the left.
* Minimum / maximum observed values depend on how many pages we keep
* attached, which depends on offscreenPageLimit.
*
* @see #setOffscreenPageLimit(int)
*/
void transformPage(@NonNull View page, float position);
}
这篇文章写了很长时间,这篇文章本身的内容也很容易跑歪,毕竟VP2和VP有相似点,里面又扯到了RecyclerView和LifeCycle,所以行文思路上有点难以集中,很跳脱(主要是我周末不想写,平时时间也不多,写写停停)。这篇文章我也尝试改变以前的行文习惯,写的也挺痛苦,以前习惯于:提出问题,给出结论,给出细节关键;这次在介绍新内容时,尝试了寻找问题,找答案式的行文,内容看起来是相当的长。以后如果遇到合适的内容,我也会尝试用更加合适的行文方式去做内容分享。