看完这一篇,ViewPager2基本可以投产了

前言

ViewPager2已经出来很长一段时间了,但之前一直都是alpha版本,几次版本迭代中,内容细节变化也挺多,前阵子第一个正式版发布,不巧新冠肺炎爆发,在家里索性把之前的预研Demo整理整理,梳理下内容点写一篇博客,也算把预研工作正式收个尾。

首先先感谢一个我确实记不得的大兄嘚,预研的demo的前身,来自于GitHub,是我很久之前看alpha版本的使用时下载的,只下了一个zip,着实找不到源头了。年前开始预研的时候,我正好发现电脑里面有一个项目,就懒得新建,直接改了一通。

先贴上本文的 demo地址 (https://github.com/leobert-lan/ViewPager2-Demo/tree/master)

本文内容梗概

本文的篇幅会比较长,先给一个大致的内容梗概:

  • 用于展示普通视图,横向和纵向滑动
  • 结合Fragment使用以及Fragment的生命周期
  • 复杂布局下,常见的结构,配合CoordinatorLayout和NestedScroll使用
  • 懒、预加载和状态恢复,主要观测生命周期
  • 实现原理和源码分析

最关键是我写写停停,有时候一篇文章拖个把月,不先放个梗概我自己都会忘了要写啥。

使用介绍

展示普通视图

按照以往用ViewPager的经验,我们会使用到三个东西:

  • 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,就可以开始使用了。

结合Fragment的使用

结合我们日常工作实际,现在单独使用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 这里就不贴了,下面我们会带着问题看生命周期

配合Lifecycle做生命周期感知

这一章节是一个引申章节,如果暂时不想将注意力移开的话,可以跳过本章节。

我们先贴几张老图:

看完这一篇,ViewPager2基本可以投产了_第1张图片 Fragment生命周期 看完这一篇,ViewPager2基本可以投产了_第2张图片 Fragment生命周期和Activity生命周期的关系

 

看完这一篇,ViewPager2基本可以投产了_第3张图片

这里列出了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);
//...
}
再顺着往下面,后面方法的代码着实太多,就不贴了

而这个方法被三处调用:

  • androidx.viewpager2.adapter.FragmentStateAdapter#gcFragments
  • androidx.viewpager2.adapter.FragmentStateAdapter#onBindViewHolder
  • androidx.viewpager2.adapter.FragmentStateAdapter#onViewRecycled

目前发现上述操作手法中,是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的文章(其实是我还没准备好内容)

 

配合CoordinatorLayout以及支持NestedScroll机制

又是一个很常见的需求了,现在只要和资讯、内容搭点边的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

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
        }
    }

FragmentStateAdapter

而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是咋玩的。

FragmentViewHolder

/**
 * {@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,看起来也没啥分析的必要了(其实就是我犯懒了)

ItemDecoration

正常使用,虽然看起来没啥必要

PageTransformer

这是一个难得的可以定制的内容,但是呢,也不是新内容,参考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,所以行文思路上有点难以集中,很跳脱(主要是我周末不想写,平时时间也不多,写写停停)。这篇文章我也尝试改变以前的行文习惯,写的也挺痛苦,以前习惯于:提出问题,给出结论,给出细节关键;这次在介绍新内容时,尝试了寻找问题,找答案式的行文,内容看起来是相当的长。以后如果遇到合适的内容,我也会尝试用更加合适的行文方式去做内容分享。

 

 

 

 

你可能感兴趣的:(android,Java,android,ViewPager2)