ViewPager2的原理和使用

一、ViewPager2介绍

1 简介

 ViewPager2是Google 在 androidx 组件包里增加的一个组件,目前已经到了1.0.0-beta02版本。

谷歌为什么要出这个组件呢?官方是这么说的:

ViewPager2 replaces ViewPager, addressing most of its predecessor’s pain-points, 
including right-to-left layout support, vertical orientation, modifiable Fragment collections, etc.

2 具体改动:

New features:

  • 支持竖向滚动

  • 完整支持notifyDataSetChanged

  • 能够关闭用户输入 (setUserInputEnabled, isUserInputEnabled)

API changes:

  • FragmentStateAdapter 替代 FragmentStatePagerAdapter

  • RecyclerView.Adapter 替代 PagerAdapter

  • registerOnPageChangeCallback 替代 addPageChangeListener

3 附上官方链接:

官方文档
https://developer.android.google.cn/jetpack/androidx/releases/viewpager2#1.0.0-alpha01

官方Demo
https://github.com/googlesamples/android-viewpager2

二、ViewPager2原理的简单介绍

ViewPager2继承ViewGroup,内部核心是RecycleView加LinearLayoutManager,其实就是对RecycleView封装了一层,所有功能都是围绕着RecyclerView和LinearLayoutManager展开,不过我们在这里不展开介绍了,我们主要关注两个点。

  • ViewPager2是怎么使用RecyclerView实现ViewPager的效果的?

  • RecyclerView来怎么配合Fragment的生命周期的

ViewPager2是怎么使用RecyclerView实现ViewPager的效果的

1、我们先从ViewPager2的初始化入手,代码如下:

 private void initialize(Context context, AttributeSet attrs) {
        mAccessibilityProvider = sFeatureEnhancedA11yEnabled
                ? new PageAwareAccessibilityProvider()
                : new BasicAccessibilityProvider();
        
        //RecyclerView基本设置 begin——————————————————
        mRecyclerView = new RecyclerViewImpl(context);
        mRecyclerView.setId(ViewCompat.generateViewId());

        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());
        //RecyclerView基本设置 end——————————————————
        
        //创建ScrollEventAdapter,此adapter作用是将RecyclerView.OnScrollListener事件转变为ViewPager2.OnPageChangeCallback事件
        mScrollEventAdapter = new ScrollEventAdapter(this);
        
        //创建模拟拖动事件
        mFakeDragger = new FakeDrag(this, mScrollEventAdapter, mRecyclerView);
        
        //创建PagerSnapHelper对象,用来拦截fling事件,从而实现ViewPager页面切换的效果
        mPagerSnapHelper = new PagerSnapHelperImpl();
        mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
      
        mRecyclerView.addOnScrollListener(mScrollEventAdapter);

        mPageChangeEventDispatcher = new CompositeOnPageChangeCallback(3);
        mScrollEventAdapter.setOnPageChangeCallback(mPageChangeEventDispatcher);

        //pdates mCurrentItem after swipes
        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();
                }
            }
        };

        // update internal state firstinternal state first
        mPageChangeEventDispatcher.addOnPageChangeCallback(currentItemUpdater);
        mAccessibilityProvider.onInitialize(mPageChangeEventDispatcher, mRecyclerView);
        mPageChangeEventDispatcher.addOnPageChangeCallback(mExternalPageChangeCallbacks);

        // Add mPageTransformerAdapter after mExternalPageChangeCallbacks, because page transform
        // events must be fired after scroll events
        //PageTransformerAdapter 作用是将页面的滑动事件转变为比率变化,比如,一个页面从左到右滑动,positionOffset变化是从0~1
        mPageTransformerAdapter = new PageTransformerAdapter(mLayoutManager);
        mPageChangeEventDispatcher.addOnPageChangeCallback(mPageTransformerAdapter);
        attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
    }

在initialize()方法里面,初始化了RecyclerView组件。

主要做了这几件事:

  • RecycleView的基本初始化
  • 设置了PagerSnapHelper,目的是实现切面切换的效果
  • 给RecyclerView设置了滑动监听事件,涉及到的组件是ScrollEventAdapter,后面的基本功能都需要这个组件的支持

ViewPager的效果,就是靠PagerSnapHelper和ScrollEventAdapter实现的。

2、PagerSnapHelper是做什么的?

PagerSnapHelper继承于SnapHelper。SnapHelper顾名思义是Snap+Helper的组合,Snap有移到某位置的含义,Helper为辅助者,
综合场景解释是 将RecyclerView移动到某位置的辅助类。
SnapHelper的内部有两个监听接口:OnFlingListener和OnScrollListener,分别用来监听RecyclerView的fling事件和scroll事件。

SnapHelper有三个重要的方法:

  • findTargetSnapPosition(计算Fling事件能滑动到位置)
  • findSnapView(找到需要显示在RecyclerView的最前面的View)
  • calculateDistanceToFinalSnap(计算需要滑动的距离)。

PagerSnapHelper重写了这三个方法,用以拦截fling事件,做到每次滑动一个item

那么这些方法什么时间调用的?

  • 手指在快速滑动,在手指离开屏幕前,三个方法均不调用。
  • 手指在快速滑后,手指离开了屏幕,会发生Fling事件,findTargetSnapPosition方法会被调用。
  • 当Fling事件结束时,RecyclerView会回调SnapHelper内部OnScrollListener接口的onScrollStateChanged方法。
    此时RecyclerView的滑动状态为RecyclerView.SCROLL_STATE_IDLE,会调用findSnapView方法来找到需要
    显示在RecyclerView的最前面的View。找到目标View之后,就会调用calculateDistanceToFinalSnap方法来计算需要滑动的距离。然后调用smoothScrollBy滑动到对应位置。

我们先从PagerSnapHelper调用的attachToRecyclerView方法说起。

SnapHelper.java

    public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
            throws IllegalStateException {
        //判断是否是同一个RecyclerView,如果是直接返回,防止重复attach
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        //如果是一个新的RecyclerView,先销毁所有的回调接口,这里指的是
        //RecyclerView的scroll监听和fling监听
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
        //保存这个RecyclerView
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
            //重新设置监听
            setupCallbacks();
            //初始化一个Scoller,用于滚动RecylerView
            mGravityScroller = new Scroller(mRecyclerView.getContext(),
                    new DecelerateInterpolator());
            //移动到指定的已存在的View 后面讲解
            snapToTargetExistingView();
        }
    }
    
      //设置回调关系
    private void setupCallbacks() throws IllegalStateException {
        if (mRecyclerView.getOnFlingListener() != null) {
            throw new IllegalStateException("An instance of OnFlingListener already set.");
        }
        mRecyclerView.addOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(this);
    }

    //注销回调关系
    private void destroyCallbacks() {
        mRecyclerView.removeOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(null);
    }
SnapHelper在attachToRecyclerView方法中先注册了滚动状态和fling的监听,当监听触发时,如何处理后续的流程?
我们先来看下onScroll事件的处理。
SnapHelper.java

 // Handles the snap on scroll case.
    private final RecyclerView.OnScrollListener mScrollListener =
            new RecyclerView.OnScrollListener() {
                boolean mScrolled = false;
 
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    //当newState == 静止状态且滚动距离不等于0,触发snapToTargetExistingView();
                    if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                        mScrolled = false;
                        snapToTargetExistingView();
                    }
                }
 
                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    if (dx != 0 || dy != 0) {
                        mScrolled = true;
                    }
                }
            };
    //移动到指定的已存在的View
     void snapToTargetExistingView() {
        if (mRecyclerView == null) {
            return;
        }
        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return;
        }
        //查找SnapView
        View snapView = findSnapView(layoutManager);
        if (snapView == null) {
            return;
        }
        //计算SnapView的距离
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
        }
    }

snapToTargetExistingView()这个方法会在第一次attach的时候和RecyclerView滑动状态改变的时候调用,用于将RecyclerView移动到指定的位置,移动的距离根据当前距离RecyclerView中心点最近的那个itemView获取,获取到这个距离后,调用smoothScrollBy方法移动RecyclerView。在这个方法中findSnapView和calculateDistanceToFinalSnap是两个抽象方法。需要子类重写,实现不同的效果。

整理一下滚动状态回调下,SnapHelper的实现流程图如下;

image.png

我们接着看下fling事件,

在RecyclerView中fling这里就不展开了。行为流程图如下:
image.png
SnapHelper.java

@Override
    public boolean onFling(int velocityX, int velocityY) {
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return false;
        }
        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        if (adapter == null) {
            return false;
        }
        //获取出发RecyclerView fling的最小速度,这是一个定义好的常量值
        int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
        //当滑动速度满足条件时,执行snapFromFling方法
        return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                && snapFromFling(layoutManager, velocityX, velocityY);
    }

然后我们来看看SnapHelper的snapFromFling方法:

SnapHelper.java

//处理snap的fling逻辑
private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        //判断layoutManager要实现ScrollVectorProvider
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return false;
        }
        //创建SmoothScroller
        RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }
        //获得snap position
        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }
          //设置position
        smoothScroller.setTargetPosition(targetPosition);
        //启动SmoothScroll
        layoutManager.startSmoothScroll(smoothScroller);
        //返回true拦截掉后续的fling操作
        return true;
    }
    
    @Nullable
    @Deprecated
    protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return null;
        }
        return new LinearSmoothScroller(mRecyclerView.getContext()) {
            @Override
            protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
                if (mRecyclerView == null) {
                    // The associated RecyclerView has been removed so there is no action to take.
                    return;
                }
                 //计算Snap到目标位置的距离
                int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
                        targetView);
                final int dx = snapDistances[0];
                final int dy = snapDistances[1];
                //计算时间
                final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                if (time > 0) {
                    action.update(dx, dy, time, mDecelerateInterpolator);
                }
            }
          //计算速度
            @Override
            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
            }
        };
    }

fling的逻辑主要在snapFromFling方法中,完成fling逻辑需要几点:

  • 要求layoutManager是ScrollVectorProvider的实现
  • 创建SmoothScroller,主要逻辑是createSnapScroller方法
  • 该方法有默认的实现,主要逻辑是创建一个LinearSmoothScroller
  • 通过findTargetSnapPosition方法获取目标targetPosition
  • 最后把targetPosition赋值给smoothScroller,通过layoutManager执行该scroller
  • snapFromFling要返回true,返回true的话,默认的ViewFlinger就不会执行。

fling流程:


image.png

从snapFromFling方法中我们知道,只要findTargetSnapPosition方法返回不为RecyclerView.NO_POSITION,那么接下来的滑动事件会交给SmoothScroller去处理,所以RecyclerView最终滑到的位置为当前位置的上一个或者下一个,不会产生Fling的效果。

为了阻止RecyclerView的Fling事件,findTargetSnapPosition方法需要保证几点

  • 返回当前ItemView的上一个ItemView或者下一个ItemView的位置
  • 保证findTargetSnapPosition方法返回的值不为RecyclerView.NO_POSITION,

查找指定的SnapPosition

/*
这个方法表示fling操作最终能滑动到I的temView的position。
这个position称为targetSnapPosition,位置上对应的View就是targetSnapView。如果找不到position,就返回RecyclerView.NO_POSITION
*/
@Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        final int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return RecyclerView.NO_POSITION;
        }

        final OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
        if (orientationHelper == null) {
            return RecyclerView.NO_POSITION;
        }

        // A child that is exactly in the center is eligible for both before and after
        View closestChildBeforeCenter = null;
        int distanceBefore = Integer.MIN_VALUE;
        View closestChildAfterCenter = null;
        int distanceAfter = Integer.MAX_VALUE;

        // Find the first view before the center, and the first view after the center
        final int childCount = layoutManager.getChildCount();
        
        // 找到与当前View相邻的View,包括左相邻和右响铃,并且计算滑动的距离
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            if (child == null) {
                continue;
            }
            final int distance = distanceToCenter(layoutManager, child, orientationHelper);

            if (distance <= 0 && distance > distanceBefore) {
                // Child is before the center and closer then the previous best
                distanceBefore = distance;
                closestChildBeforeCenter = child;
            }
            if (distance >= 0 && distance < distanceAfter) {
                // Child is after the center and closer then the previous best
                distanceAfter = distance;
                closestChildAfterCenter = child;
            }
        }

        // 根据滑动的方向来返回的相应位置
        final boolean forwardDirection = isForwardFling(layoutManager, velocityX, velocityY);
        if (forwardDirection && closestChildAfterCenter != null) {
            return layoutManager.getPosition(closestChildAfterCenter);
        } else if (!forwardDirection && closestChildBeforeCenter != null) {
            return layoutManager.getPosition(closestChildBeforeCenter);
        }

        // 兜底计算
        View visibleView = forwardDirection ? closestChildBeforeCenter : closestChildAfterCenter;
        if (visibleView == null) {
            return RecyclerView.NO_POSITION;
        }
        int visiblePosition = layoutManager.getPosition(visibleView);
        int snapToPosition = visiblePosition
                + (isReverseLayout(layoutManager) == forwardDirection ? -1 : +1);

        if (snapToPosition < 0 || snapToPosition >= itemCount) {
            return RecyclerView.NO_POSITION;
        }
        return snapToPosition;
    }

当RecyclerView滑动完毕之后,此时会先调用findSnapView方法获取来最终位置的ItemView。当RecyclerView触发Fling事件时,才会触发findTargetSnapPosition方法,从而保证RecyclerView滑动到正确位置;那么当RecyclerView没有触发Fling事件,怎么保证RecyclerView滑动到正确位置呢?当然是findSnapView方法和calculateDistanceToFinalSnap方法,这俩方法还有一个目的就是,如果Fling没有滑动正确位置,这俩方法可以做一个兜底操作。

那么findSnapView和calculateDistanceToFinalSnap究竟怎么做的?

我们先来看PagerSnapHelper重写的findSnapView方法。

PagerSnapHelper.java

  @Nullable
    @Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        //判断RecyclerView的滑动方向,这个可以由layoutManager获取
        if (layoutManager.canScrollVertically()) {
            //OrientationHelper是对RecycleView中子View管理的工具类
            return findCenterView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
        }
        return null;
    }
    /**
     * Return the child view that is currently closest to the center of this parent.
     * 如注释所说,这个方法返回的是一个距离当前parent也就是RecyclerView中心位置最近的一个item
     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
     *                      {@link RecyclerView}.
     * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
     *
     * @return the child view that is currently closest to the center of this parent.
     */
    @Nullable
    private View findCenterView(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }
 
        View closestChild = null;
        final int center;
        //clipToPadding是RecyclerView的一个属性,表示在含有padding值的时候
        //会不会裁剪padding位置的view,如果为true,表示裁剪,那么在padding
        //位置滚动的时候是看不到view的
        if (layoutManager.getClipToPadding()) {
            //getStartAfterPadding  获取RecycleView左侧内边距(paddingLeft)
            //getTotalSpace  Recycleview水平内容区大小(宽度,除去左右内边距)
            //这些方法都会在OrientationHelper中具体说明,如果是Horizontal,
            //getStartAfterPadding  得到的是paddingLeft,如果是vertical,那么得到
            //的是paddingTop,自己想象一下
            //含有padding的时候,RecyclerView的中心位置应该是起始位置的padding
            //值加上内容区的宽度的一半,得到的刚好是RecyclerView显示内容的中心
            //位置的值
            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            //没有padding,则中心位置的值直接是宽度的一半
            center = helper.getEnd() / 2;
        }
        int absClosest = Integer.MAX_VALUE;
 
        //循环的计算所有childView,得到离上边计算到的中心点值最近的一个
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            //getDecoratedStart返回view左边界点(包含左内边距和左外边距)在父View中的位置(以父View的(0,0)点位坐标系)
            //通俗地讲:子View左边界点到父View的(0,0)点的水平间距
            ///getDecoratedMeasurement返回view在水平方向上所占位置的大小(包括view的左右外边距)
            int childCenter = helper.getDecoratedStart(child)
                    + (helper.getDecoratedMeasurement(child) / 2);
            int absDistance = Math.abs(childCenter - center);
 
            /** if child center is closer than previous closest, set it as closest  **/
            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }
        return closestChild;
    }

findCenterView方法还是比较长,但是表示的意思非常简单,就是找到当前中心距离屏幕中心最近的ItemView。这个怎么来理解呢?比如说,我们手指在滑动一个页面,滑动到一定距离时就松开了,此时屏幕当中有两个页面,那么ViewPager2应该滑动到哪一个页面呢?当然是距离屏幕中心最近的页面。findCenterView方法的作用便是如此。

经过上边的计算,就能得到距离当前RecyclerView中心位置最近的一个itemView。
找到需要滑到的ItemView,此时就应该调用calculateDistanceToFinalSnap方法来计算,此时RecyclerView还需要滑动多少距离才能达到正确位置。
    @Nullable
    @Override
    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView) {
        int[] out = new int[2];
        //判断方向,获取方向上移动的值,保存在数组中返回
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToCenter(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }
 
        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }
 
    private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView, OrientationHelper helper) {
        //getDecoratedStart 返回view左边界点(包含左内边距和左外边距)在父View中的位置(以父View的(0,0)点位坐标系)
            //通俗地讲:子View左边界点到父View的(0,0)点的水平间距
        final int childCenter = helper.getDecoratedStart(targetView)
                + (helper.getDecoratedMeasurement(targetView) / 2);
        final int containerCenter;
        if (layoutManager.getClipToPadding()) {
            //获取RecycleView左侧内边距(paddingLeft)
            //再次计算RecyclerView中心位置
            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            containerCenter = helper.getEnd() / 2;
        }
        //用itemView的中心位置减去RecyclerView的中心位置,得到的就是
        //将要移动的距离
        return childCenter - containerCenter;
    }

calculateDistanceToFinalSnap表达的意思非常简单,就是计算RecyclerView需要滑动的距离,主要通过distanceToCenter方法来计算。PagerSnapHelper的移动规则是每次滑动将距离中心位置最近的item移动到中心位置.

3、ScrollEventAdapter转换事件

继承RecyclerView.OnScrollListener,作用是将RecyclerView.OnScrollListener(滑动事件)转变为ViewPager2.OnPageChangeCallback(页面滑动事件)。

ScrollEventAdapter有几种状态值:

名称 含义
STATE_IDLE 表示当前ViewPager2处于停止状态
STATE_IN_PROGRESS_MANUAL_DRAG 表示当前ViewPager2处于手指拖动状态
STATE_IN_PROGRESS_SMOOTH_SCROLL 表示当前ViewPager2处于缓慢滑动的状态。这个状态只在调用了ViewPager2的setCurrentItem方法才有可能出现。
STATE_IN_PROGRESS_IMMEDIATE_SCROLL 表示当前ViewPager2处于迅速滑动的状态。这个状态只在调用了ViewPager2的setCurrentItem方法才有可能出现。
STATE_IN_PROGRESS_FAKE_DRAG 表示当前ViewPager2未使用手指滑动,而是通过FakerDrag实现的。

ScrollEventAdapter的重心是重写onScrollStateChanged()和onScrolled()
,当RecyclerView的滑动状态发生变化,onScrollStateChanged()方法就会被调用。

onScrollStateChanged()方法

主要有三个步骤:

  • 开始拖动,会调用startDrag方法表示拖动开始。
  • 拖拽释放,此时ViewPager2会准备滑动到正确的位置。
  • 滑动结束,此时ScrollEventAdapter会调用相关的方法更新状态。
    @Override
    public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
        // 1、用户开始拖拽
        if ((mAdapterState != STATE_IN_PROGRESS_MANUAL_DRAG
                || mScrollState != SCROLL_STATE_DRAGGING)
                && newState == RecyclerView.SCROLL_STATE_DRAGGING) {
            startDrag(false);
            return;
        }

        // 2、拖拽释放 ,此时ViewPager2会准备滑动到正确的位置 
        //RecyclerView is snapping to page (dragging -> settling)
        // Note that mAdapterState is not updated, to remember we were dragging when settling
        if (isInAnyDraggingState() && newState == RecyclerView.SCROLL_STATE_SETTLING) {
            // Only go through the settling phase if the drag actually moved the page
            if (mScrollHappened) {
                /**
                将状态改变的信息分发到OnPageChangeCallback监听器,不过需要注意的是:当ViewPager2处于停止状态,同时调用了setCurrentItem方法来立即切换到某一个页面(注意,不是缓慢的切换),不会回调OnPageChangeCallback的方法。
                */
                dispatchStateChanged(SCROLL_STATE_SETTLING);
                // Determine target page and dispatch onPageSelected on next scroll event
                mDispatchSelected = true;
            }
            return;
        }

        // 3、拖拽结束 此时ScrollEventAdapter会调用相关的方法更新状态 Drag is finished (dragging || settling -> idle)
        if (isInAnyDraggingState() && newState == RecyclerView.SCROLL_STATE_IDLE) {
            boolean dispatchIdle = false;
            updateScrollEventValues();
             // 如果在拖动期间为产生移动距离
            if (!mScrollHappened) {
                // Pages didn't move during drag, so either we're at the start or end of the list,
                // or there are no pages at all.
                // In the first case, ViewPager's contract requires at least one scroll event.
                // In the second case, don't send that scroll event
                if (mScrollValues.mPosition != RecyclerView.NO_POSITION) {
                    dispatchScrolled(mScrollValues.mPosition, 0f, 0);
                }
                dispatchIdle = true;
            } else if (mScrollValues.mOffsetPx == 0) {
                // Normally we dispatch the selected page and go to idle in onScrolled when
                // mOffsetPx == 0, but in this case the drag was still ongoing when onScrolled was
                // called, so that didn't happen. And since mOffsetPx == 0, there will be no further
                // scroll events, so fire the onPageSelected event and go to idle now.
                // Note that if we _did_ go to idle in that last onScrolled event, this code will
                // not be executed because mAdapterState has been reset to STATE_IDLE.
                dispatchIdle = true;
                if (mDragStartPosition != mScrollValues.mPosition) {
                    dispatchSelected(mScrollValues.mPosition);
                }
            }
            if (dispatchIdle) {
                // Normally idle is fired in last onScrolled call, but either onScrolled was never
                // called, or we were still dragging when the last onScrolled was called
                dispatchStateChanged(SCROLL_STATE_IDLE);
                resetState();
            }
        }
    }

在这个方法中,前两点我们都知道,我们主要看第三个步骤。
dispatchStateChanged方法会在什么时候调用?

  • 没有滑动,也就是说,onScrolled方法没有被调用;
  • 滑动过,并且在上一次滑动中最后一次调用onScrolled方法的时候会被调用。

dispatchSelected方法会在什么时候调用?

  • 当mOffsetPx为0时会被调用,mOffsetPx为0表示当前ViewPager2根本未滑动。

onScrolled()方法


 @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        mScrollHappened = true;
          // 更新相关值
        updateScrollEventValues();

        if (mDispatchSelected) {
           // 拖动手势释放,ViewPager2正在滑动到正确的位置
            // Drag started settling, need to calculate target page and dispatch onPageSelected now
            mDispatchSelected = false;
            boolean scrollingForward = dy > 0 || (dy == 0 && dx < 0 == mViewPager.isRtl());

            // "&& values.mOffsetPx != 0": filters special case where we're scrolling forward and
            // the first scroll event after settling already got us at the target
            mTarget = scrollingForward && mScrollValues.mOffsetPx != 0
                    ? mScrollValues.mPosition + 1 : mScrollValues.mPosition;
            if (mDragStartPosition != mTarget) {
                dispatchSelected(mTarget);
            }
        } else if (mAdapterState == STATE_IDLE) {
          
            // onScrolled while IDLE means RV has just been populated after an adapter has been set.
            // Contract requires us to fire onPageSelected as well.
            int position = mScrollValues.mPosition;
            // Contract forbids us to send position = -1 though
            // 调用了setAdapter方法
            dispatchSelected(position == NO_POSITION ? 0 : position);
        }

        // If position = -1, there are no items. Contract says to send position = 0 instead.
        dispatchScrolled(mScrollValues.mPosition == NO_POSITION ? 0 : mScrollValues.mPosition,
                mScrollValues.mOffset, mScrollValues.mOffsetPx);

        // Dispatch idle in onScrolled instead of in onScrollStateChanged because RecyclerView
        // doesn't send IDLE event when using setCurrentItem(x, false)
        // 因为调用了setCurrentItem(x, false)不会触发IDLE状态的产生,所以需要在这里
        // 调用dispatchStateChanged方法
        if ((mScrollValues.mPosition == mTarget || mTarget == NO_POSITION)
                && mScrollValues.mOffsetPx == 0 && !(mScrollState == SCROLL_STATE_DRAGGING)) {
            // When the target page is reached and the user is not dragging anymore, we're settled,
            // so go to idle.
            // Special case and a bit of a hack when mTarget == NO_POSITION: RecyclerView is being
            // initialized and fires a single scroll event. This flags mScrollHappened, so we need
            // to reset our state. However, we don't want to dispatch idle. But that won't happen;
            // because we were already idle.
            dispatchStateChanged(SCROLL_STATE_IDLE);
            resetState();
        }
    }

onScrolled方法里面主要做了两件事:

  • 调用updateScrollEventValues方法更新ScrollEventValues里面的值(mPosition,mOffset,mOffsetPx)。
  • 调用相关方法,更新状态。

对ScrollEventAdapter总结下:

  • 当调用ViewPager2的setAdapter方法时,此时应该回调一次dispatchSelected方法。
  • 当调用setCurrentItem(x, false)方法,不会调用onScrollStateChanged方法,因而不会产生idle状态,因此,我们需要在onScrolled方法特殊处理(onScrolled方法会被调用)。
  • 正常的拖动和释放,就是onScrollStateChanged方法和onScrolled方法的正常回调。

经过ScrollEventAdapter的转换,将RecyclerView.OnScrollListener(滑动事件)转变为ViewPager2.OnPageChangeCallback(页面滑动事件),那么OnPageChangeCallback怎么处理呢?

ViewPager2中还有一个adapter——PageTransformerAdapter,作用将OnPageChangeCallback的事件转换成为一种特殊的事件,什么特殊的事件呢?

我以一个例子来解释一下:

  • 假设ViewPager2此时从A页面滑动到B页面,并且是从右往左滑动,其中A页面的变化范围:[0,-1);B页面的变化范围:[1,0)。
  • 假设ViewPager2此时从B页面滑动到A页面,并且是从左往右滑动,其中A页面的变化范围:[-1,0);B页面的变化范围:[0,1)。
 @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (mPageTransformer == null) {
            return;
        }

        float transformOffset = -positionOffset;
        for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
            View view = mLayoutManager.getChildAt(i);
            if (view == null) {
                throw new IllegalStateException(String.format(Locale.US,
                        "LayoutManager returned a null child at pos %d/%d while transforming pages",
                        i, mLayoutManager.getChildCount()));
            }
            int currPos = mLayoutManager.getPosition(view);
            float viewOffset = transformOffset + (currPos - position);
            mPageTransformer.transformPage(view, viewOffset);
        }
    }

FragmentStateAdapter与Fragment生命周期

  • 变量
    //key为itemId,value为Fragment。表示position与所放Fragment的对应关系(itemId与position有对应关系)
    final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
    //key为itemId,value为Fragment的状态
    private final LongSparseArray<Fragment.SavedState> mSavedStates = new LongSparseArray<>();
    //key为itemId, value为ItemView的id。
    private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>();
  • 方法
    • onCreateViewHolder()
    • onBindViewHolder()
    • onViewAttachedToWindow()
    • onViewRecycled()
    • onFailedToRecycleView()

(1). onCreateViewHolder方法

onCreateViewHolder方法主要创建ViewHolder,其实就是调用了FragmentViewHolder的一个静态方法.

    @NonNull
    @Override
    public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return FragmentViewHolder.create(parent);
    }

      //创建一个宽高都MATCH_PARENT的FrameLayout,注意这里并不像PagerAdapter是Fragment的rootView;
    @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);
    }

(2). onBindViewHolder方法

onBindViewHolder方法是将Fragment加载到ItemView上,但是因为RecyclerView的复用机制,所以有很多的条件判断。

我们先看一下代码:

    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
        // 如果当前ItemView已经加载了Fragment,并且不是同一个Fragment
        // 那么就移除
        if (boundItemId != null && boundItemId != itemId) {
            removeFragment(boundItemId);
            mItemIdToViewHolder.remove(boundItemId);
        }

        mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
        // 保证对应位置的Fragment已经初始化,并且放在mFragments中
        ensureFragment(position);

        final FrameLayout container = holder.getContainer();
        // 特殊情况,当RecyclerView让ItemView保持在Window,
        // 但是不在视图树中。
        if (ViewCompat.isAttachedToWindow(container)) {
            if (container.getParent() != null) {
                throw new IllegalStateException("Design assumption violated.");
            }
            // 当ItemView添加在到RecyclerView中才加载Fragment
            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
                        placeFragmentInViewHolder(holder);
                    }
                }
            });
        }

        gcFragments();
    }
onBindViewHolder方法主要分为三步:
  • 当前ItemView上已经加载了Fragment,并且不是同一个Fragment(ItemView被复用了),那么先移除掉ItemView上的Fragment
  • 初始化相关信息。
  • 如果存在特殊情况,会走特殊情况。正常来说,都会经过onAttachToWindow方法来对Fragment进行加载。
  • 每次调用都会gc一次,主要的避免用户修改数据源造成垃圾对象

(3). onViewAttachedToWindow方法

正常情况下,ItemView会在这个方法里面加载Fragment。

    @Override
    public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
        placeFragmentInViewHolder(holder);
        gcFragments();
    }

和onBindViewHolder一样,调用了placeFragmentInViewHolder方法加载Fragment。

(4). onViewRecycled方法

当ViewHolder被回收,到回收池中的时候,onViewRecycled方法会被调用。
而在onViewRecycled方法里面,会对Fragment进行remove。

    @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);
        }
    }
这里为什么会在onViewRecycled方法来对Fragment进行remove,而不在onViewDetachedFromWindow方法进行remove呢。

当onViewRecycled方法被调用,表示当前ViewHolder已经彻底没有用了,需要被放入回收池,等待被复用。
此时存在的情况可能有:

  • 1.当前ItemView手动移除掉了
  • 2.当前位置对应的视图已经彻底不在屏幕中,被当前屏幕中某些位置复用了。

所以在onViewRecycled方法里面移除Fragment比较合适。

而onViewDetachedFromWindow方法,每次一个页面被滑走,都会被调用,如果对其Fragment进行卸载,此时又滑回来,又要重新加载一次,这性能就下降了很多。
onFailedToRecycleView方法与onViewRecycled方法差不多,不在介绍。

(5). placeFragmentInViewHolder方法

placeFragmentInViewHolder方法主要是加载Fragment,是整个FragmentStateAdapter的核心点。

在加载Fragment之前,我们需要判断几个状态:

  • Fragment是否添加到ItemView 中
  • Fragment的View是否已经创建
  • Fragment的View 是否添加视图树中

placeFragmentInViewHolder方法中一共有8种情况。

我们来看看代码:

 void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) {

        Fragment fragment = mFragments.get(holder.getItemId());
        if (fragment == null) {
            throw new IllegalStateException("Design assumption violated.");
        }
        FrameLayout container = holder.getContainer();
        View view = fragment.getView();

        /*
        possible states:
        - fragment: { added, notAdded }
        - view: { created, notCreated }
        - view: { attached, notAttached }

        combinations:
        - { f:added, v:created, v:attached } -> check if attached to the right container
        - { f:added, v:created, v:notAttached} -> attach view to container
        - { f:added, v:notCreated, v:attached } -> impossible
        - { f:added, v:notCreated, v:notAttached} -> schedule callback for when created
        - { f:notAdded, v:created, v:attached } -> illegal state
        - { f:notAdded, v:created, v:notAttached } -> illegal state
        - { f:notAdded, v:notCreated, v:attached } -> impossible
        - { f:notAdded, v:notCreated, v:notAttached } -> add, create, attach
         */

        // { f:notAdded, v:created, v:attached } -> illegal state
        // { f:notAdded, v:created, v:notAttached } -> illegal state
        
        //——————————————————————————————————————————————————————
        
        // 1.Fragment未添加到ItemView中,但是View已经创建
        // 非法状态
        if (!fragment.isAdded() && view != null) {
            throw new IllegalStateException("Design assumption violated.");
        }

        // 2.Fragment添加到ItemView中,但是View未创建
        // 先等待View创建完成,然后将View添加到Container。
        if (fragment.isAdded() && view == null) {
            scheduleViewAttach(fragment, container);
            return;
        }

        // 3.Fragment添加到ItemView中,同时View已经创建完成并且添加到Container中
        // 需要保证View添加到正确的Container中。
        if (fragment.isAdded() && view.getParent() != null) {
            if (view.getParent() != container) {
                addViewToContainer(view, container);
            }
            return;
        }

        // 4.Fragment添加到ItemView中,同时View已经创建完成但是未添加到Container中
        // 需要将View添加到Container中。
        if (fragment.isAdded()) {
            addViewToContainer(view, container);
            return;
        }

        // 5.Fragment未创建,View未创建、未添加
        if (!shouldDelayFragmentTransactions()) {
            scheduleViewAttach(fragment, container);
            mFragmentManager.beginTransaction().add(fragment, "f" + holder.getItemId()).commitNow();
        } else {
            //第六步
            // 调用了第5步,但是Fragment还未真正创建
            if (mFragmentManager.isDestroyed()) {
                return; // nothing we can do
            }
            mLifecycle.addObserver(new GenericLifecycleObserver() {
                @Override
                public void onStateChanged(@NonNull LifecycleOwner source,
                        @NonNull Lifecycle.Event event) {
                    if (shouldDelayFragmentTransactions()) {  // 7
                        return;
                    }
                    source.getLifecycle().removeObserver(this);
                    if (ViewCompat.isAttachedToWindow(holder.getContainer())) { // 8
                        placeFragmentInViewHolder(holder);
                    }
                }
            });
        }
    }
  • FragmentStateAdapter在遇到预加载时,只会创建Fragment对象,不会把Fragment真正的加入到布局中,所以自带懒加载效果
  • FragmentStateAdapter不会一直保留Fragment实例,回收的ItemView也会移除Fragment,所以得做好Fragment`重建后恢复数据的准备
  • FragmentStateAdapter在遇到offscreenPageLimit>0时,处理离屏Fragment和可见Fragment没有什么区别,所以无法通过setUserVisibleHint判断显示与否

总结

ViewPager2本身是一个ViewGroup,只是封装一个RecyclerView。

  • PagerSnapHelper实现页面切换效果:

    • calculateDistanceToFinalSnap
    • findSnapView
    • findTargetSnapPosition
  • ScrollEventAdapter将RecyclerView的滑动事件转换成为ViewPager2的页面滑动事件。

  • PageTransformerAdapter的作用将普通的页面滑动事件转换为特殊事件。

  • FragmentStateAdapter中使用Adapter加载Fragment,也考虑到了ViewHolder的复用,Fragment加载和remove。

三、ViewPager2的使用方式

https://www.jianshu.com/p/25aa5cacbfb9

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容