相关文章推荐
阳刚的凉面  ·  thymeleaf ...·  3 月前    · 
俊逸的小马驹  ·  Azure Database for ...·  1 年前    · 
谦逊的猴子  ·  typeorm - How do I ...·  1 年前    · 
精彩文章免费看

RecyclerView扩展(六) - RecyclerView平滑滑动的实现原理

时隔一年多,我又来更新RecyclerView相关的文章,感觉上一篇RecyclerView相关文章的完成就在昨天(手动狗头)。今天,我们来学习一下RecyclerView内部的 smoothScroll 相关方法的原理。
在这之前,我先说一下这篇文章的背景。最近在做一个RecyclerView相关的需求,用到了平滑滑动相关的方法,在开发中发现了,Google爸爸提供的api不能满足我们的要求。于是我就想到了,去看一下相关的源码,然后自己实现。自以为是的认为对RecyclerView的源码比较了解,但是当自己真正看源码的时候,才发现自己想的太天真了,平滑滑动的原理远远没有那么的简单。最后在公司一位大佬的指点下,实现了想要的效果。在实现了效果之后,心中对这一块的原理充满了兴趣,毕竟之前在系统性学习RecyclerView源码,对这部分的知识一直是忽略的。所以,本文就由此产生了。
注意,本文RecycclerView相关源码均来自于1.2.0-alpha03版本。

1. 概述

在分析源码之前,我们先来看看RecyclerView平滑滑动的相关API吧。从功能上区分,RecyclerView相关的API主要分为两部分: smoothScrollBy smoothScrollToPosition 。其中, smoothScrollBy 方法滑动指定的距离, smoothScrollToPosition 表示滑动到指定位置的ItemView。
我们可以先从宏观上思考这两个方法的实现。 smoothScrollBy 方法很简单,因为知道了滑动的距离,那么使用OverScroller实现即可;那么 smoothScrollToPosition 方法是怎么实现的呢?我们都知道,我们想要滑动到的位置上的ItemView有可能还没有加到RecyclerView,那么RecyclerView是怎么知道滑动多少距离呢?这是本文需要分析的一个问题。
同时,我们知道,在 RecyclerView 的LinearLayoutManager中,有一个 scrollToPositionWithOffset 方法,但是没有一个 smoothScrollToPositionWithOffset 方法。换句话说,如果我们想要一个平滑滑动到某一个位置之后再多滑一点距离,通过现在的接口是不能实现的。本文会通过分析 SmoothScroller 类,进而实现一个类似的接口方法。

2. smoothScrollBy方法的实现原理

在分析 smoothScrollBy 方法之前,我先解释一下为啥先分析它。因为 smoothScrollToPosition 方法在滑动时,最后也是通过该方法实现的,所以,我们理解了 smoothScrollBy 的实现之后,对 smoothScrollToPosition 方法的理解就有一大半了。
我们先来看一下 smoothScrollBy 方法的实现:

    void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator,
            int duration, boolean withNestedScrolling) {
        // ······
        if (!mLayout.canScrollHorizontally()) {
            dx = 0;
        if (!mLayout.canScrollVertically()) {
            dy = 0;
        if (dx != 0 || dy != 0) {
            boolean durationSuggestsAnimation = duration == UNDEFINED_DURATION || duration > 0;
            if (durationSuggestsAnimation) {
                if (withNestedScrolling) {
                    int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                    if (dx != 0) {
                        nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                    if (dy != 0) {
                        nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                    startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
                mViewFlinger.smoothScrollBy(dx, dy, duration, interpolator);
            } else {
                scrollBy(dx, dy);

  smoothScrollBy的代码很简单,滑动最终走到了ViewFlinger的smoothScrollBy方法。我们再来看看ViewFlinger的smoothScrollBy方法:

        public void smoothScrollBy(int dx, int dy, int duration,
                @Nullable Interpolator interpolator) {
            // Handle cases where parameter values aren't defined.
            if (duration == UNDEFINED_DURATION) {
                duration = computeScrollDuration(dx, dy, 0, 0);
            if (interpolator == null) {
                interpolator = sQuinticInterpolator;
            // If the Interpolator has changed, create a new OverScroller with the new
            // interpolator.
            if (mInterpolator != interpolator) {
                mInterpolator = interpolator;
                mOverScroller = new OverScroller(getContext(), interpolator);
            // Reset the last fling information.
            mLastFlingX = mLastFlingY = 0;
            // Set to settling state and start scrolling.
            setScrollState(SCROLL_STATE_SETTLING);
            mOverScroller.startScroll(0, 0, dx, dy, duration);
            // ······
            postOnAnimation();

  别看smoothScrollBy方法有这么多的代码,其实做的都是一件事,初始化各种信息,包括滑动距离、滑动时间和滑动的插值器等。触发滑动的是通过调用postOnAnimation方法的,而postOnAnimation方法本身没有做什么事,就是任务队列中增加一个Runnable,保证下一次绘制会执行。那么下一次绘制会执行那个方法呢?别忘了ViewFlinger本身一个是Runnable,所以执行的肯定是它的run方法。
  我们来简单的看一下run方法吧,为啥说简单看一下run方法,因为run方法本身比较复杂,涉及的方面有很多,本文就不深入的探讨,有兴趣的可以看看:RecyclerView 源码分析(二) - RecyclerView的滑动机制。两年前的文章,大家将就看吧...(androidX对RecyclerView滑动的实现改动挺大的)。

        public void run() {
            // ······
            final OverScroller scroller = mOverScroller;
            //1. 判断是否需要滑动
            if (scroller.computeScrollOffset()) {
                 // 2. 处理滑动
                 // ······
                // 3.判断是否是否结束
                if (!smoothScrollerPending && doneScrolling) {
                   // ······
                } else {
                    // Otherwise continue the scroll. 
                    postOnAnimation();
                    // ······
            // ······

  总的来说,run方法实现平滑滑动的过程,我将它分为3步:

  • 首先通过调用OvserScroller的computeScrollOffset方法来判断还有可以滑动的距离。如果可以滑动的距离,那么computeScrollOffset方法返回的true,此时我们可以通过getCurrX方法或者getCurrY方法获取最新的滑动位置。
  • 处理滑动。RecyclerView在处理滑动比较复杂时,这里面包括对嵌套滑动的分发,以及对LayoutManger的回调实现自己的滑动,还包括我们后面要说的SmoothScroller也是在这里被回调的。这里先不对这部分的代码做过多的谈论,后面在分析SmoothScroller时,会分析其中一部分。说句题外话,这部分的代码时RecyclerView对滑动处理的核心代码,有兴趣的同学可以看看。
  • 判断是否滑动结束。这里的滑动结束包含多种含义,我们可以将它分为两部分:正常结束和非正常结束。其中,正常结束表示的意思是,平滑滑动或者fling滑动自然的结束,即滑动速度为0;非正常滑动结束表示的意思是,RecyclerView不能再滑动了,被强制停止了,比如说RecyclerView滑动到底部或者顶部,但是滑动速度不为0。如果滑动没有结束,那就正常的执行,继续调用postOnAnimation方法,触发下一次滑动。
  •   可有人会有疑问,为啥调用postOnAnimation方法会触发下一次滑动呢?这个就得说说OverScroller的原理。我简单的解释一下OvserScroller吧。

    其实OvserScroller本身不参与滑动的任何操作,它对外就有一个作用--产生滑动距离。这个怎么理解呢?比如说,如果我们想要在1s内从0滑动到100,那么OvserScroller就要在这1s内产生具体的滑动距离。是不是感觉这个跟属性滑动中的ValueAnimator很相似?但是它们俩有一个不同:ValueAnimator是主动产生的所有数值,就是说我们调用了start方法之后,ValueAnimator就开始为我们产生一系列的数值;而OvserScroller是被动产生数值的,它什么时候产生数值,取决于我们什么时候去调用computeScrollOffset方法,这个computeScrollOffset方法就是用来更新和产生数值的,而OvserScroller的start方法就只做了一件事:记录信息。这也是为啥,我们需要递归的调用computeScrollOffset原因。

      如上便是smoothScrollBy方法的实现原理,是不是很简单?接下来,我们将迎来本文的主角--smoothScrollToPosition方法。

    3. smoothScrollToPosition方法

      在分析smoothScrollToPosition方法之前,我先提一个问题:我们都知道smoothScrollToPosition方法是指滑动到指定的位置,那么RecyclerView怎么知道已经滑动到这个View呢?换句话说,RecyclerView怎么知道要滑动多少距离呢?我们都知道,如果ItemView不在屏幕中,我们是不知道它的位置的。
      有人可能会回答,那还不简单,通过如上的递归方式滑动,每次滑动之后都判断指定位置的ItemView是否已经出现在屏幕中,如果已经在屏幕中,表示已经滑动到目的地了,可以停止滑动了。是的,简单来说RecyclerView就是这么实现的!但是大家使用smoothScrollToPosition方法之后会知道一个特性,就是将要滑动目的地时,RecyclerView会减速,上面的方式好像不行,所以RecyclerView是怎么实现这个效果呢?这是接下来的内容要解答的问题之一。我汇总一下,我们需要知道答案的问题:

    RecyclerView是怎么通过递归方式滑动到指定位置的? RecyclerView是怎么知道什么时候可以开始减速的?

    (1). 开始滑动

      好了,废话扯的差不多了,接下来我们就从源码上寻找我们想要的答案吧。首先来看一下smoothScrollToPosition方法的源码:

        public void smoothScrollToPosition(int position) {
            if (mLayoutSuppressed) {
                return;
            if (mLayout == null) {
                Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
                        + "Call setLayoutManager with a non-null argument.");
                return;
            mLayout.smoothScrollToPosition(this, mState, position);
    

      RecyclerView的smoothScrollToPosition方法很简单,直接调用了LayoutManager的smoothScrollToPosition方法,这里我们就看一下LinearLayoutManagersmoothScrollToPosition吧(其实StaggeredGridLayoutManagerLinearLayoutManager的实现是一样的)。

        @Override
        public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
                int position) {
            LinearSmoothScroller linearSmoothScroller =
                    new LinearSmoothScroller(recyclerView.getContext());
            linearSmoothScroller.setTargetPosition(position);
            startSmoothScroll(linearSmoothScroller);
    

      smoothScrollToPosition方法主要做的事是,创建一个LinearSmoothScroller对象,然后调用了startSmoothScroll方法。看上去好像并没有做什么事,其实不然,这里创建的LinearSmoothScroller对象非常的重要,smoothScrollToPosition的实现全靠这个类来实现的;同时在创建对象的时候,我们可以看到通过调用setTargetPosition设置目标的位置,这一点也非常的重要。我们再来看看startSmoothScroll方法:

            public void startSmoothScroll(SmoothScroller smoothScroller) {
                if (mSmoothScroller != null && smoothScroller != mSmoothScroller
                        && mSmoothScroller.isRunning()) {
                    mSmoothScroller.stop();
                mSmoothScroller = smoothScroller;
                mSmoothScroller.start(mRecyclerView, this);
    

      startSmoothScroll方法一共做了三件事:

  • 如果之前已经在滑动了,会将它停止。
  • 将新的SmoothScroller对象赋值给mSmoothScroller。大家要记得这一步操作,因为后面的内容我们经常看见它。
  • 调用start方法。这个方法的作用就是触发滑动。
  •   我们看一下start方法的实现:

            void start(RecyclerView recyclerView, LayoutManager layoutManager) {
                // Stop any previous ViewFlinger animations now because we are about to start a new one.
                recyclerView.mViewFlinger.stop();
                if (mStarted) {
                    Log.w(TAG, "An instance of " + this.getClass().getSimpleName() + " was started "
                            + "more than once. Each instance of" + this.getClass().getSimpleName() + " "
                            + "is intended to only be used once. You should create a new instance for "
                            + "each use.");
                mRecyclerView = recyclerView;
                mLayoutManager = layoutManager;
                if (mTargetPosition == RecyclerView.NO_POSITION) {
                    throw new IllegalArgumentException("Invalid target position");
                mRecyclerView.mState.mTargetPosition = mTargetPosition;
                mRunning = true;
                mPendingInitialRun = true;
                mTargetView = findViewByPosition(getTargetPosition());
                onStart();
                mRecyclerView.mViewFlinger.postOnAnimation();
                mStarted = true;
    

      start方法的作用很简单,就是记录滑动需要的信息,其中包括设置mTargetPosition;将mPendingInitialRun设置为true;寻找mTargetView,这个点也非常的重要,如果此时距离TargetView还非常的远,这里返回的就是null,如果不为null,那么就表示即将滑动到TargetView。这个为null或者不为null是非常的重要,这个决定后面应该怎么滑动(决定是继续快速滑动还是减速滑动)。
      最后,就是调用ViewFlinger的postOnAnimation方法开始滑动。看到这里,我们不禁有一个疑问了,这里我们并不知道需要滑动的距离,咋就开始滑动了呢?针对这个疑问,我们去ViewFlinger的run方法中去寻找答案:

            @Override
            public void run() {
                // ······
                final OverScroller scroller = mOverScroller;
                if (scroller.computeScrollOffset()) {
                   // ······
                SmoothScroller smoothScroller = mLayout.mSmoothScroller;
                // call this after the onAnimation is complete not to have inconsistent callbacks etc.
                if (smoothScroller != null && smoothScroller.isPendingInitialRun()) {
                    smoothScroller.onAnimation(0, 0);
                // ······
    

      一般来说,当我们调用smoothScrollToPosition触发了run方法的执行时,computeScrollOffset方法都是返回为false(这里就不对特殊case做分析了),因为在这之前,我们没有调用OverScroller的start方法。那么是怎么触发滑动的呢?答案就在下面调用的SmoothScrolleronAnimation方法。从前面的分析,我们知道,我们通过调用smoothScrollToPosition方法,这里SmoothScroller肯定不为null,同时isPendingInitialRun方法肯定也为true,这个在前面已经特别说明了。所以,我们来看看onAnimation方法:

            void onAnimation(int dx, int dy) {
                // ······
                // The following if block exists to have the LayoutManager scroll 1 pixel in the correct
                // direction in order to cause the LayoutManager to draw two pages worth of views so
                // that the target view may be found before scrolling any further.  This is done to
                // prevent an initial scroll distance from scrolling past the view, which causes a
                // jittery looking animation.
                // 1. 先滑动1像素。
                if (mPendingInitialRun && mTargetView == null && mLayoutManager != null) {
                    PointF pointF = computeScrollVectorForPosition(mTargetPosition);
                    if (pointF != null && (pointF.x != 0 || pointF.y != 0)) {
                        recyclerView.scrollStep(
                                (int) Math.signum(pointF.x),
                                (int) Math.signum(pointF.y),
                                null);
                mPendingInitialRun = false;
                // 2. TargetView即将滑到
                if (mTargetView != null) {
                    // verify target position
                    if (getChildPosition(mTargetView) == mTargetPosition) {
                        onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
                        mRecyclingAction.runIfNecessary(recyclerView);
                        stop();
                    } else {
                        Log.e(TAG, "Passed over target position while smooth scrolling.");
                        mTargetView = null;
                // 3. TargetView还未滑到。
                if (mRunning) {
                    onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction);
                    boolean hadJumpTarget = mRecyclingAction.hasJumpTarget();
                    mRecyclingAction.runIfNecessary(recyclerView);
                    if (hadJumpTarget) {
                        // It is not stopped so needs to be restarted
                        if (mRunning) {
                            mPendingInitialRun = true;
                            recyclerView.mViewFlinger.postOnAnimation();
    

      onAnimation方法里面主要分为三步,如上面的注释,我们分别看一下:

  • 如果TargetView不为null,先滑动1像素。这样的做目的是处理一个特殊的case,假设我们屏幕中有5个ItemView,并且第5个ItemView的底部恰好跟RecyclerView底部对齐,此时如果我们想要滑动到第6个ItemView,能保证在下一次滑动中看到TargetView,从而执行下面的减速滑动(在实际情况中,RecyclerView是有预加载的,这里假设RecyclerView没有预加载,也就是假设RecyclerView的ItemView没有在屏幕中,是不会加载的,即TargetView为null)
  • TargetView不为null,表示已经ItemView已经滑动到屏幕中,即将完整展示,此时就会开始减速滑动。从这里我们找到上面本小节前面提的两个问题中的第二个问题。这里还有一个小细节,就是调用stop方法,表示快速滑动的SmoothScroller对象已经停止滑动,这个对象就是我们在LinearLayoutManagersmoothScrollToPosition方法创建的对象。大家应该可以从我的描述中得到一些信息,没错,减速滑动是通过另一个SmoothScroller对象实现的,这里就会创建,只不过是在这里调用的方法里面创建的,并不是onAnimation方法里面。
  • 如果当前的SmoothScroller还在继续滑动,就是执行另一部分的操作。这里之所以特指继续滑动,是因为上面在执行减速滑动时,会调用stop方法。所以,如果上面执行了减速滑动,这里就不会执行。
  •   这里我们先来看看第三步吧。上面解释了第3步会执行另一部分的操作,而这里说的另一部分的操作,是指的啥呢?我们主要看两个方法:onSeekTargetStep方法和runIfNecessary方法。
      我们先来看看onSeekTargetStep方法,这里以LinearSmoothScroller为例:

        protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
            // TODO(b/72745539): Is there ever a time when onSeekTargetStep should be called when
            // getChildCount returns 0?  Should this logic be extracted out of this method such that
            // this method is not called if getChildCount() returns 0?
            if (getChildCount() == 0) {
                stop();
                return;
            //noinspection PointlessBooleanExpression
            if (DEBUG && mTargetVector != null
                    && (mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0)) {
                throw new IllegalStateException("Scroll happened in the opposite direction"
                        + " of the target. Some calculations are wrong");
            mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
            mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
            if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
                updateActionForInterimTarget(action);
            } // everything is valid, keep going
    

      onSeekTargetStep方法的作用就是计算SmoothScroller还可以滑动多少距离,其中dy表示本次滑动消耗的距离,mInterimTargetDxmInterimTargetDy表示一共需要滑动的距离。因为我们这里是第一次调用onSeekTargetStep方法,也就是说dy为0,同时mInterimTargetDxmInterimTargetDy也为0。同时mInterimTargetDy如果为0,但是dy不为0,表示不是第一次调用,而是指滑动距离消耗完毕了。总的来说,第一次调用或者距离消耗完毕都会调用updateActionForInterimTarget方法。
      那么updateActionForInterimTarget方法里面做了啥事呢?我们来看看:

        protected void updateActionForInterimTarget(Action action) {
            // find an interim target position
            PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
            if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
                final int target = getTargetPosition();
                action.jumpTo(target);
                stop();
                return;
            normalize(scrollVector);
            mTargetVector = scrollVector;
            mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
            mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
            final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
            // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
            // interim target. Since we track the distance travelled in onSeekTargetStep callback, it
            // won't actually scroll more than what we need.
            action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
                    (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
                    (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
    

      updateActionForInterimTarget方法看上去挺复杂的,但是实际上就是做了两件事:

  • 计算mInterimTargetDxmInterimTargetDy,以滑动时间的time。这两个变量,我们前面已经见过了,表示的是可以滑动的距离。同时需要注意的是,这俩的值是固定!!!要么为12000,要么为-12000,是不是挺有意思的?
  • 同时将计算的值更新到Action里面。Action是SmoothScroller的内部类,主要的作用是记录SmoothScroller滑动需要的滑动距离(即Dx和Dy)、滑动时间(即time)、滑动插值器(即mInterpolator)。快速滑动和最后的减速滑动就是因为这个插值器不同导致的。这里更新Action信息的操作非常的重要。
  •   到这里,我们应该知道onSeekTargetStep方法干了什么事吧。我简单总结一下吧,onSeekTargetStep方法里面主要做了2件事:

  • 更新mInterimTargetDxmInterimTargetDx,由于前面有可能滑动了一定的距离,所以这里需要更新,这样后面的滑动才知道还有多少距离。
  • 当滑动距离消耗完了或者是第一次调用,会调用updateActionForInterimTarget方法,重新给出新的滑动距离,并且记录在Action里面。
  •   经过onSeekTargetStep方法之后,RecyclerView知道了新的滑动距离之后,此时就是调用ActionrunIfNecessary方法了。我们来看看这个方法:

                void runIfNecessary(RecyclerView recyclerView) {
                    // ······
                    if (mChanged) {
                        validate();
                        recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration, mInterpolator);
                        mConsecutiveUpdates++;
                        if (mConsecutiveUpdates > 10) {
                            // A new action is being set in every animation step. This looks like a bad
                            // implementation. Inform developer.
                            Log.e(TAG, "Smooth Scroll action is being updated too frequently. Make sure"
                                    + " you are not changing it unless necessary");
                        mChanged = false;
                    } else {
                        mConsecutiveUpdates = 0;
    

      runIfNecessary方法比较简单,就是先看看Action的信息是否被更新过,如果更新过,就调用smoothScrollBy方法触发滑动;如果没有被更新过,那么什么都不做。在这里,我多说几句:

  • 如果mChanged为true,即Action的信息被更新表示两种情况:1. 这是第一次滑动;2.前面的滑动已经完成了,这里会触发一次新的滑动。mChanged设置为true,这个在前面我们已经介绍了,就是在Action的update方法中操作的。需要的注意的是,这里的Dy就是滑动需要的距离,如果TargetView为null的话,mDx和mDy就是为12000或者-12000;如果TargetView不为null,mDx和mDy就表示具体的距离。
  • 如果mChanged不为true调用到这里的话,表示不需要重新触发滑动,这是为啥呢?如果mChanged不为true,表示当前的滑动还未结束,即还有可滑动的距离,此时ViewFlinger在执行run方法时,会自己调用postOnAnimation方法。这个在前面分析smoothScrollBy时,我们已经了解到了。
  • (2). 滑动中

      经过上面一小节,我们知道,如果才开始滑动的话,滑动距离是12000像素(这里就以正数为例)。那么接下来就是正常的滑动,正常的滑动就如上面分析smoothScrollBy一样,就是通过递归的方式从OverScroller里面获取最新的滑动位置,然后开始滑动。
      不过,这里还是跟之前的分析有不同的地方,我们来看看:

                    if (mAdapter != null) {
                        // ······
                        // If SmoothScroller exists, this ViewFlinger was started by it, so we must
                        // report back to SmoothScroller.
                        SmoothScroller smoothScroller = mLayout.mSmoothScroller;
                        if (smoothScroller != null && !smoothScroller.isPendingInitialRun()
                                && smoothScroller.isRunning()) {
                            final int adapterSize = mState.getItemCount();
                            if (adapterSize == 0) {
                                smoothScroller.stop();
                            } else if (smoothScroller.getTargetPosition() >= adapterSize) {
                                smoothScroller.setTargetPosition(adapterSize - 1);
                                smoothScroller.onAnimation(consumedX, consumedY);
                            } else {
                                smoothScroller.onAnimation(consumedX, consumedY);
    

      如果我们通过smoothScrollToPosition方法触发了run方法的执行,那么在每次滑动执行之后,都会调用onAnimation方法,来告知SmoothScroller本次滑动了一部分的距离,进而SmoothScroller 会更新相关的信息,执行一些其他的操作,比如说滑动结束了,触发了新的滑动,或者TargetView滑动到屏幕中了,开始减速滑动。
      上面的点非常重要,SmoothScroller要随时知道滑动的状态,因为SmoothScroller可能随时改变滑动的策略。这个滑动策略改变主要从滑动结束说起,接下来我们就看看滑动结束的情况。

    (3).滑动结束

      一般来说,每次onAnimation的调用都有可能表示滑动结束,那么怎么来区分它们呢?我们将滑动结束分为两类:

  • 被动结束。前面已经说了,smoothScrollToPosition方法一次滑动12000像素,如果RecyclerView还没有到我们想要的位置呢?此时调用onAnimation方法时,SmoothScroller就会知道本次滑动的滑动距离已经消耗完毕了,然后产生新的滑动距离,也是12000像素,重新触发一次滑动。这个在前面分析 onSeekTargetStep方法已经说了,这里就不过多的分析了。这就是上面提的第一个问题答案。
  • 主动结束。这种情况是ItemView已经滑动到屏幕中,此时调用onAnimation方法,SmoothScroller就会停止本次滑动,开始新的一次滑动,即减速滑动。需要注意的是,此时RecyclerView已经知道了具体的滑动距离,即不用调用onSeekTargetStep方法产生12000像素的距离。
  •   本小节就是重点分析主动结束的情况,也就是可以寻找到上面提的第二个问题的答案。我们直接来看看onAnimation方法:

            void onAnimation(int dx, int dy) {
                // ······
                if (mTargetView != null) {
                    // verify target position
                    if (getChildPosition(mTargetView) == mTargetPosition) {
                        onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
                        mRecyclingAction.runIfNecessary(recyclerView);
                        stop();
                    } else {
                        Log.e(TAG, "Passed over target position while smooth scrolling.");
                        mTargetView = null;
                // ······
    

      在onAnimation方法中,主动结束主要做了三件事:

  • 调用onTargetFound方法,表示当ItemView即将滑到屏幕中。同时从LinearSmoothScrolleronTargetFound方法的实现,我们知道它内部实际上对Action进行了更新,即更新可以滑动距离,滑动需要的时间,以及滑动需要的插值器(减速的插值器)。
  • 调用runIfNecessary方法触发一个新的滑动。从这里,我们可以对onAnimation方法对runIfNecessary方法做一个简单的总结,就是在调用runIfNecessary方法,都需要对Action内部的信息进行更新,只不过这里是调用onTargetFound方法,正常滑动时调用onSeekTargetStep方法。
  • 调用stop方法,表示当前快速滑动已经结束。这里的调用能避免onAnimation方法下面的操作执行。
  •   我们来看看onTargetFound做了哪些事:

        protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
            final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
            final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
            final int distance = (int) Math.sqrt(dx * dx + dy * dy);
            final int time = calculateTimeForDeceleration(distance);
            if (time > 0) {
                action.update(-dx, -dy, time, mDecelerateInterpolator);
    

      onTargetFound方法主要做了3件事:

  • 调用calculateDxToMakeVisible方法,计算可以滑动的距离,即滑动到目标ItemView需要的距离。在calculateDxToMakeVisible内部调用calculateDtToFit方法真正返回滑动所需的距离。关于calculateDtToFit方法,后面自定义实现smoothScrollToPositionWithOffset方法是会使用到,这里就不过多的讨论了。
  • 调用calculateTimeForDeceleration方法,计算减速滑动需要的时间。
  • 调用Action的updte方法,更新相关的信息。在这里,我们传递了一个DecelerateInterpolator对象,这个就是减速使用的插值器。
  •   至此,我们就知道,RecyclerView在不知道滑动距离的情况下,是怎么通过smoothScrollToPosition方法滑动到具体的ItemView。待会,我会做一个简单的总结,在这里,我们先学以致用,实现一个smoothScrollToPositionWithOffset方法。

    4. 实现smoothScrollToPositionWithOffset方法

      我们知道,不管是RecyclerView还是LayoutManger,都没有这个方法供我们使用,那么如果我们有这个要求,自己怎么实现呢?其实很简单的,我们直接上代码:

        fun smoothScrollToPositionWithOffset(position: Int, offset: Int) {
            layoutManager?.let {
                val smoothScroller = object : LinearSmoothScroller(context) {
                    override fun calculateDtToFit(
                        viewStart: Int,
                        viewEnd: Int,
                        boxStart: Int,
                        boxEnd: Int,
                        snapPreference: Int
                    ): Int {
                        val rawOffset =
                            super.calculateDtToFit(viewStart, viewEnd, boxStart, boxEnd, snapPreference)
                        return rawOffset - offset;
                smoothScroller.targetPosition = position
                it.startSmoothScroll(smoothScroller)
    

      其实,实现的本质就是通过重写LinearSmoothScrollercalculateDtToFit方法,我们在前面已经知道了,calculateDtToFit方法就是计算滑动到TargetView还需要多少的距离。我们的实现就是在它的基础加上我们想要的offset就行了,是不是很简单?
      同时SmoothScroller还是很多其他的方法,我们可以自定义或者重写,实现我们想要的效果。不得不说,RecyclerView这一块的扩展太大了!!!

    5. 总结

      到这里,本文就结束了,我在这里对本文的内容做一个简单的总结。

  • RecyclerView平滑滑动提供了两个方法:smoothScrollBysmoothScrollToPosition。其中smoothScrollBy表示滑动具体的距离;smoothScrollToPosition表示滑动到具体的位置。
  • smoothScrollBy是通过递归实现的,主要依靠OverScroller完成滑动位置的计算。 smoothScrollToPosition可以分解为多个smoothScrollBy的滑动,每次滑动12000像素。当一次滑动结束之后,会重新触发一次新的12000像素的滑动;当在某一次滑动中,发现TargetView出现在屏幕中了,会立即停止当前的滑动,开始一个减速滑动。
    最后编辑于:2020-05-22 13:47