纵有疾风起
人生不言弃

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

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

1. 概述

  在分析源码之前,我们先来看看RecyclerView平滑滑动的相关API吧。从功能上区分,RecyclerView相关的API主要分为两部分:smoothScrollBysmoothScrollToPosition。其中,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步:

  1. 首先通过调用OvserScroller的computeScrollOffset方法来判断还有可以滑动的距离。如果可以滑动的距离,那么computeScrollOffset方法返回的true,此时我们可以通过getCurrX方法或者getCurrY方法获取最新的滑动位置。
  2. 处理滑动。RecyclerView在处理滑动比较复杂时,这里面包括对嵌套滑动的分发,以及对LayoutManger的回调实现自己的滑动,还包括我们后面要说的SmoothScroller也是在这里被回调的。这里先不对这部分的代码做过多的谈论,后面在分析SmoothScroller时,会分析其中一部分。说句题外话,这部分的代码时RecyclerView对滑动处理的核心代码,有兴趣的同学可以看看。
  3. 判断是否滑动结束。这里的滑动结束包含多种含义,我们可以将它分为两部分:正常结束和非正常结束。其中,正常结束表示的意思是,平滑滑动或者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是怎么实现这个效果呢?这是接下来的内容要解答的问题之一。我汇总一下,我们需要知道答案的问题:

  1. RecyclerView是怎么通过递归方式滑动到指定位置的?
  2. 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方法一共做了三件事:

  1. 如果之前已经在滑动了,会将它停止。
  2. 将新的SmoothScroller对象赋值给mSmoothScroller。大家要记得这一步操作,因为后面的内容我们经常看见它。
  3. 调用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方法里面主要分为三步,如上面的注释,我们分别看一下:

  1. 如果TargetView不为null,先滑动1像素。这样的做目的是处理一个特殊的case,假设我们屏幕中有5个ItemView,并且第5个ItemView的底部恰好跟RecyclerView底部对齐,此时如果我们想要滑动到第6个ItemView,能保证在下一次滑动中看到TargetView,从而执行下面的减速滑动(在实际情况中,RecyclerView是有预加载的,这里假设RecyclerView没有预加载,也就是假设RecyclerView的ItemView没有在屏幕中,是不会加载的,即TargetView为null)
  2. TargetView不为null,表示已经ItemView已经滑动到屏幕中,即将完整展示,此时就会开始减速滑动。从这里我们找到上面本小节前面提的两个问题中的第二个问题。这里还有一个小细节,就是调用stop方法,表示快速滑动的SmoothScroller对象已经停止滑动,这个对象就是我们在LinearLayoutManagersmoothScrollToPosition方法创建的对象。大家应该可以从我的描述中得到一些信息,没错,减速滑动是通过另一个SmoothScroller对象实现的,这里就会创建,只不过是在这里调用的方法里面创建的,并不是onAnimation方法里面。
  3. 如果当前的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方法看上去挺复杂的,但是实际上就是做了两件事:

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

  到这里,我们应该知道onSeekTargetStep方法干了什么事吧。我简单总结一下吧,onSeekTargetStep方法里面主要做了2件事:

  1. 更新mInterimTargetDxmInterimTargetDx,由于前面有可能滑动了一定的距离,所以这里需要更新,这样后面的滑动才知道还有多少距离。
  2. 当滑动距离消耗完了或者是第一次调用,会调用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方法触发滑动;如果没有被更新过,那么什么都不做。在这里,我多说几句:

  1. 如果mChanged为true,即Action的信息被更新表示两种情况:1. 这是第一次滑动;2.前面的滑动已经完成了,这里会触发一次新的滑动。mChanged设置为true,这个在前面我们已经介绍了,就是在Action的update方法中操作的。需要的注意的是,这里的Dy就是滑动需要的距离,如果TargetView为null的话,mDx和mDy就是为12000或者-12000;如果TargetView不为null,mDx和mDy就表示具体的距离。
  2. 如果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的调用都有可能表示滑动结束,那么怎么来区分它们呢?我们将滑动结束分为两类:

  1. 被动结束。前面已经说了,smoothScrollToPosition方法一次滑动12000像素,如果RecyclerView还没有到我们想要的位置呢?此时调用onAnimation方法时,SmoothScroller就会知道本次滑动的滑动距离已经消耗完毕了,然后产生新的滑动距离,也是12000像素,重新触发一次滑动。这个在前面分析 onSeekTargetStep方法已经说了,这里就不过多的分析了。这就是上面提的第一个问题答案。
  2. 主动结束。这种情况是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方法中,主动结束主要做了三件事:

  1. 调用onTargetFound方法,表示当ItemView即将滑到屏幕中。同时从LinearSmoothScrolleronTargetFound方法的实现,我们知道它内部实际上对Action进行了更新,即更新可以滑动距离,滑动需要的时间,以及滑动需要的插值器(减速的插值器)。
  2. 调用runIfNecessary方法触发一个新的滑动。从这里,我们可以对onAnimation方法对runIfNecessary方法做一个简单的总结,就是在调用runIfNecessary方法,都需要对Action内部的信息进行更新,只不过这里是调用onTargetFound方法,正常滑动时调用onSeekTargetStep方法。
  3. 调用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件事:

  1. 调用calculateDxToMakeVisible方法,计算可以滑动的距离,即滑动到目标ItemView需要的距离。在calculateDxToMakeVisible内部调用calculateDtToFit方法真正返回滑动所需的距离。关于calculateDtToFit方法,后面自定义实现smoothScrollToPositionWithOffset方法是会使用到,这里就不过多的讨论了。
  2. 调用calculateTimeForDeceleration方法,计算减速滑动需要的时间。
  3. 调用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. 总结

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

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

文章转载于:https://www.jianshu.com/p/c2e7d8a1ec5c

原著是一个有趣的人,若有侵权,请通知删除

未经允许不得转载:起风网 » RecyclerView扩展(六) – RecyclerView平滑滑动的实现原理
分享到: 生成海报

评论 抢沙发

评论前必须登录!

立即登录