RecyclerView擴(kuò)展(六) - RecyclerView平滑滑動(dòng)的實(shí)現(xiàn)原理

??時(shí)隔一年多,我又來(lái)更新RecyclerView相關(guān)的文章,感覺(jué)上一篇RecyclerView相關(guān)文章的完成就在昨天(手動(dòng)狗頭)。今天,我們來(lái)學(xué)習(xí)一下RecyclerView內(nèi)部的smoothScroll相關(guān)方法的原理。
??在這之前,我先說(shuō)一下這篇文章的背景。最近在做一個(gè)RecyclerView相關(guān)的需求,用到了平滑滑動(dòng)相關(guān)的方法,在開(kāi)發(fā)中發(fā)現(xiàn)了,Google爸爸提供的api不能滿足我們的要求。于是我就想到了,去看一下相關(guān)的源碼,然后自己實(shí)現(xiàn)。自以為是的認(rèn)為對(duì)RecyclerView的源碼比較了解,但是當(dāng)自己真正看源碼的時(shí)候,才發(fā)現(xiàn)自己想的太天真了,平滑滑動(dòng)的原理遠(yuǎn)遠(yuǎn)沒(méi)有那么的簡(jiǎn)單。最后在公司一位大佬的指點(diǎn)下,實(shí)現(xiàn)了想要的效果。在實(shí)現(xiàn)了效果之后,心中對(duì)這一塊的原理充滿了興趣,畢竟之前在系統(tǒng)性學(xué)習(xí)RecyclerView源碼,對(duì)這部分的知識(shí)一直是忽略的。所以,本文就由此產(chǎn)生了。
??注意,本文RecycclerView相關(guān)源碼均來(lái)自于1.2.0-alpha03版本。

1. 概述

??在分析源碼之前,我們先來(lái)看看RecyclerView平滑滑動(dòng)的相關(guān)API吧。從功能上區(qū)分,RecyclerView相關(guān)的API主要分為兩部分:smoothScrollBysmoothScrollToPosition。其中,smoothScrollBy方法滑動(dòng)指定的距離,smoothScrollToPosition表示滑動(dòng)到指定位置的ItemView。
??我們可以先從宏觀上思考這兩個(gè)方法的實(shí)現(xiàn)。smoothScrollBy方法很簡(jiǎn)單,因?yàn)橹懒嘶瑒?dòng)的距離,那么使用OverScroller實(shí)現(xiàn)即可;那么smoothScrollToPosition方法是怎么實(shí)現(xiàn)的呢?我們都知道,我們想要滑動(dòng)到的位置上的ItemView有可能還沒(méi)有加到RecyclerView,那么RecyclerView是怎么知道滑動(dòng)多少距離呢?這是本文需要分析的一個(gè)問(wèn)題。
??同時(shí),我們知道,在RecyclerView的LinearLayoutManager中,有一個(gè)scrollToPositionWithOffset方法,但是沒(méi)有一個(gè)smoothScrollToPositionWithOffset方法。換句話說(shuō),如果我們想要一個(gè)平滑滑動(dòng)到某一個(gè)位置之后再多滑一點(diǎn)距離,通過(guò)現(xiàn)在的接口是不能實(shí)現(xiàn)的。本文會(huì)通過(guò)分析SmoothScroller類,進(jìn)而實(shí)現(xiàn)一個(gè)類似的接口方法。

2. smoothScrollBy方法的實(shí)現(xiàn)原理

??在分析smoothScrollBy方法之前,我先解釋一下為啥先分析它。因?yàn)?code>smoothScrollToPosition方法在滑動(dòng)時(shí),最后也是通過(guò)該方法實(shí)現(xiàn)的,所以,我們理解了smoothScrollBy的實(shí)現(xiàn)之后,對(duì)smoothScrollToPosition方法的理解就有一大半了。
??我們先來(lái)看一下smoothScrollBy方法的實(shí)現(xiàn):

    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的代碼很簡(jiǎn)單,滑動(dòng)最終走到了ViewFlinger的smoothScrollBy方法。我們?cè)賮?lái)看看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方法有這么多的代碼,其實(shí)做的都是一件事,初始化各種信息,包括滑動(dòng)距離、滑動(dòng)時(shí)間和滑動(dòng)的插值器等。觸發(fā)滑動(dòng)的是通過(guò)調(diào)用postOnAnimation方法的,而postOnAnimation方法本身沒(méi)有做什么事,就是任務(wù)隊(duì)列中增加一個(gè)Runnable,保證下一次繪制會(huì)執(zhí)行。那么下一次繪制會(huì)執(zhí)行那個(gè)方法呢?別忘了ViewFlinger本身一個(gè)是Runnable,所以執(zhí)行的肯定是它的run方法。
??我們來(lái)簡(jiǎn)單的看一下run方法吧,為啥說(shuō)簡(jiǎn)單看一下run方法,因?yàn)閞un方法本身比較復(fù)雜,涉及的方面有很多,本文就不深入的探討,有興趣的可以看看:RecyclerView 源碼分析(二) - RecyclerView的滑動(dòng)機(jī)制。兩年前的文章,大家將就看吧...(androidX對(duì)RecyclerView滑動(dòng)的實(shí)現(xiàn)改動(dòng)挺大的)。

        public void run() {
            // ······
            final OverScroller scroller = mOverScroller;
            //1. 判斷是否需要滑動(dòng)
            if (scroller.computeScrollOffset()) {
                 // 2. 處理滑動(dòng)
                 // ······
                // 3.判斷是否是否結(jié)束
                if (!smoothScrollerPending && doneScrolling) {
                   // ······
                } else {
                    // Otherwise continue the scroll. 
                    postOnAnimation();
                    // ······
            }
            // ······
        }

??總的來(lái)說(shuō),run方法實(shí)現(xiàn)平滑滑動(dòng)的過(guò)程,我將它分為3步:

  1. 首先通過(guò)調(diào)用OvserScroller的computeScrollOffset方法來(lái)判斷還有可以滑動(dòng)的距離。如果可以滑動(dòng)的距離,那么computeScrollOffset方法返回的true,此時(shí)我們可以通過(guò)getCurrX方法或者getCurrY方法獲取最新的滑動(dòng)位置。
  2. 處理滑動(dòng)。RecyclerView在處理滑動(dòng)比較復(fù)雜時(shí),這里面包括對(duì)嵌套滑動(dòng)的分發(fā),以及對(duì)LayoutManger的回調(diào)實(shí)現(xiàn)自己的滑動(dòng),還包括我們后面要說(shuō)的SmoothScroller也是在這里被回調(diào)的。這里先不對(duì)這部分的代碼做過(guò)多的談?wù)?,后面在分?code>SmoothScroller時(shí),會(huì)分析其中一部分。說(shuō)句題外話,這部分的代碼時(shí)RecyclerView對(duì)滑動(dòng)處理的核心代碼,有興趣的同學(xué)可以看看。
  3. 判斷是否滑動(dòng)結(jié)束。這里的滑動(dòng)結(jié)束包含多種含義,我們可以將它分為兩部分:正常結(jié)束和非正常結(jié)束。其中,正常結(jié)束表示的意思是,平滑滑動(dòng)或者fling滑動(dòng)自然的結(jié)束,即滑動(dòng)速度為0;非正?;瑒?dòng)結(jié)束表示的意思是,RecyclerView不能再滑動(dòng)了,被強(qiáng)制停止了,比如說(shuō)RecyclerView滑動(dòng)到底部或者頂部,但是滑動(dòng)速度不為0。如果滑動(dòng)沒(méi)有結(jié)束,那就正常的執(zhí)行,繼續(xù)調(diào)用postOnAnimation方法,觸發(fā)下一次滑動(dòng)。

??可有人會(huì)有疑問(wèn),為啥調(diào)用postOnAnimation方法會(huì)觸發(fā)下一次滑動(dòng)呢?這個(gè)就得說(shuō)說(shuō)OverScroller的原理。我簡(jiǎn)單的解釋一下OvserScroller吧。

其實(shí)OvserScroller本身不參與滑動(dòng)的任何操作,它對(duì)外就有一個(gè)作用--產(chǎn)生滑動(dòng)距離。這個(gè)怎么理解呢?比如說(shuō),如果我們想要在1s內(nèi)從0滑動(dòng)到100,那么OvserScroller就要在這1s內(nèi)產(chǎn)生具體的滑動(dòng)距離。是不是感覺(jué)這個(gè)跟屬性滑動(dòng)中的ValueAnimator很相似?但是它們倆有一個(gè)不同:ValueAnimator是主動(dòng)產(chǎn)生的所有數(shù)值,就是說(shuō)我們調(diào)用了start方法之后,ValueAnimator就開(kāi)始為我們產(chǎn)生一系列的數(shù)值;而OvserScroller是被動(dòng)產(chǎn)生數(shù)值的,它什么時(shí)候產(chǎn)生數(shù)值,取決于我們什么時(shí)候去調(diào)用computeScrollOffset方法,這個(gè)computeScrollOffset方法就是用來(lái)更新和產(chǎn)生數(shù)值的,而OvserScroller的start方法就只做了一件事:記錄信息。這也是為啥,我們需要遞歸的調(diào)用computeScrollOffset原因。

??如上便是smoothScrollBy方法的實(shí)現(xiàn)原理,是不是很簡(jiǎn)單?接下來(lái),我們將迎來(lái)本文的主角--smoothScrollToPosition方法。

3. smoothScrollToPosition方法

??在分析smoothScrollToPosition方法之前,我先提一個(gè)問(wèn)題:我們都知道smoothScrollToPosition方法是指滑動(dòng)到指定的位置,那么RecyclerView怎么知道已經(jīng)滑動(dòng)到這個(gè)View呢?換句話說(shuō),RecyclerView怎么知道要滑動(dòng)多少距離呢?我們都知道,如果ItemView不在屏幕中,我們是不知道它的位置的。
??有人可能會(huì)回答,那還不簡(jiǎn)單,通過(guò)如上的遞歸方式滑動(dòng),每次滑動(dòng)之后都判斷指定位置的ItemView是否已經(jīng)出現(xiàn)在屏幕中,如果已經(jīng)在屏幕中,表示已經(jīng)滑動(dòng)到目的地了,可以停止滑動(dòng)了。是的,簡(jiǎn)單來(lái)說(shuō)RecyclerView就是這么實(shí)現(xiàn)的!但是大家使用smoothScrollToPosition方法之后會(huì)知道一個(gè)特性,就是將要滑動(dòng)目的地時(shí),RecyclerView會(huì)減速,上面的方式好像不行,所以RecyclerView是怎么實(shí)現(xiàn)這個(gè)效果呢?這是接下來(lái)的內(nèi)容要解答的問(wèn)題之一。我匯總一下,我們需要知道答案的問(wèn)題:

  1. RecyclerView是怎么通過(guò)遞歸方式滑動(dòng)到指定位置的?
  2. RecyclerView是怎么知道什么時(shí)候可以開(kāi)始減速的?

(1). 開(kāi)始滑動(dòng)

??好了,廢話扯的差不多了,接下來(lái)我們就從源碼上尋找我們想要的答案吧。首先來(lái)看一下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方法很簡(jiǎn)單,直接調(diào)用了LayoutManager的smoothScrollToPosition方法,這里我們就看一下LinearLayoutManagersmoothScrollToPosition吧(其實(shí)StaggeredGridLayoutManagerLinearLayoutManager的實(shí)現(xiàn)是一樣的)。

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

??smoothScrollToPosition方法主要做的事是,創(chuàng)建一個(gè)LinearSmoothScroller對(duì)象,然后調(diào)用了startSmoothScroll方法。看上去好像并沒(méi)有做什么事,其實(shí)不然,這里創(chuàng)建的LinearSmoothScroller對(duì)象非常的重要,smoothScrollToPosition的實(shí)現(xiàn)全靠這個(gè)類來(lái)實(shí)現(xiàn)的;同時(shí)在創(chuàng)建對(duì)象的時(shí)候,我們可以看到通過(guò)調(diào)用setTargetPosition設(shè)置目標(biāo)的位置,這一點(diǎn)也非常的重要。我們?cè)賮?lái)看看startSmoothScroll方法:

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

??startSmoothScroll方法一共做了三件事:

  1. 如果之前已經(jīng)在滑動(dòng)了,會(huì)將它停止。
  2. 將新的SmoothScroller對(duì)象賦值給mSmoothScroller。大家要記得這一步操作,因?yàn)楹竺娴膬?nèi)容我們經(jīng)??匆?jiàn)它。
  3. 調(diào)用start方法。這個(gè)方法的作用就是觸發(fā)滑動(dòng)。

??我們看一下start方法的實(shí)現(xiàn):

        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方法的作用很簡(jiǎn)單,就是記錄滑動(dòng)需要的信息,其中包括設(shè)置mTargetPosition;將mPendingInitialRun設(shè)置為true;尋找mTargetView,這個(gè)點(diǎn)也非常的重要,如果此時(shí)距離TargetView還非常的遠(yuǎn),這里返回的就是null,如果不為null,那么就表示即將滑動(dòng)到TargetView。這個(gè)為null或者不為null是非常的重要,這個(gè)決定后面應(yīng)該怎么滑動(dòng)(決定是繼續(xù)快速滑動(dòng)還是減速滑動(dòng))。
??最后,就是調(diào)用ViewFlinger的postOnAnimation方法開(kāi)始滑動(dòng)。看到這里,我們不禁有一個(gè)疑問(wèn)了,這里我們并不知道需要滑動(dòng)的距離,咋就開(kāi)始滑動(dòng)了呢?針對(duì)這個(gè)疑問(wèn),我們?nèi)iewFlinger的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);
            }
            // ······
        }

??一般來(lái)說(shuō),當(dāng)我們調(diào)用smoothScrollToPosition觸發(fā)了run方法的執(zhí)行時(shí),computeScrollOffset方法都是返回為false(這里就不對(duì)特殊case做分析了),因?yàn)樵谶@之前,我們沒(méi)有調(diào)用OverScroller的start方法。那么是怎么觸發(fā)滑動(dòng)的呢?答案就在下面調(diào)用的SmoothScrolleronAnimation方法。從前面的分析,我們知道,我們通過(guò)調(diào)用smoothScrollToPosition方法,這里SmoothScroller肯定不為null,同時(shí)isPendingInitialRun方法肯定也為true,這個(gè)在前面已經(jīng)特別說(shuō)明了。所以,我們來(lái)看看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. 先滑動(dòng)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,先滑動(dòng)1像素。這樣的做目的是處理一個(gè)特殊的case,假設(shè)我們屏幕中有5個(gè)ItemView,并且第5個(gè)ItemView的底部恰好跟RecyclerView底部對(duì)齊,此時(shí)如果我們想要滑動(dòng)到第6個(gè)ItemView,能保證在下一次滑動(dòng)中看到TargetView,從而執(zhí)行下面的減速滑動(dòng)(在實(shí)際情況中,RecyclerView是有預(yù)加載的,這里假設(shè)RecyclerView沒(méi)有預(yù)加載,也就是假設(shè)RecyclerView的ItemView沒(méi)有在屏幕中,是不會(huì)加載的,即TargetView為null)
  2. TargetView不為null,表示已經(jīng)ItemView已經(jīng)滑動(dòng)到屏幕中,即將完整展示,此時(shí)就會(huì)開(kāi)始減速滑動(dòng)。從這里我們找到上面本小節(jié)前面提的兩個(gè)問(wèn)題中的第二個(gè)問(wèn)題。這里還有一個(gè)小細(xì)節(jié),就是調(diào)用stop方法,表示快速滑動(dòng)的SmoothScroller對(duì)象已經(jīng)停止滑動(dòng),這個(gè)對(duì)象就是我們?cè)?code>LinearLayoutManager的smoothScrollToPosition方法創(chuàng)建的對(duì)象。大家應(yīng)該可以從我的描述中得到一些信息,沒(méi)錯(cuò),減速滑動(dòng)是通過(guò)另一個(gè)SmoothScroller對(duì)象實(shí)現(xiàn)的,這里就會(huì)創(chuàng)建,只不過(guò)是在這里調(diào)用的方法里面創(chuàng)建的,并不是onAnimation方法里面。
  3. 如果當(dāng)前的SmoothScroller還在繼續(xù)滑動(dòng),就是執(zhí)行另一部分的操作。這里之所以特指繼續(xù)滑動(dòng),是因?yàn)樯厦嬖趫?zhí)行減速滑動(dòng)時(shí),會(huì)調(diào)用stop方法。所以,如果上面執(zhí)行了減速滑動(dòng),這里就不會(huì)執(zhí)行。

??這里我們先來(lái)看看第三步吧。上面解釋了第3步會(huì)執(zhí)行另一部分的操作,而這里說(shuō)的另一部分的操作,是指的啥呢?我們主要看兩個(gè)方法:onSeekTargetStep方法和runIfNecessary方法。
??我們先來(lái)看看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方法的作用就是計(jì)算SmoothScroller還可以滑動(dòng)多少距離,其中dy表示本次滑動(dòng)消耗的距離,mInterimTargetDxmInterimTargetDy表示一共需要滑動(dòng)的距離。因?yàn)槲覀冞@里是第一次調(diào)用onSeekTargetStep方法,也就是說(shuō)dy為0,同時(shí)mInterimTargetDxmInterimTargetDy也為0。同時(shí)mInterimTargetDy如果為0,但是dy不為0,表示不是第一次調(diào)用,而是指滑動(dòng)距離消耗完畢了。總的來(lái)說(shuō),第一次調(diào)用或者距離消耗完畢都會(huì)調(diào)用updateActionForInterimTarget方法。
??那么updateActionForInterimTarget方法里面做了啥事呢?我們來(lái)看看:

    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方法看上去挺復(fù)雜的,但是實(shí)際上就是做了兩件事:

  1. 計(jì)算mInterimTargetDxmInterimTargetDy,以滑動(dòng)時(shí)間的time。這兩個(gè)變量,我們前面已經(jīng)見(jiàn)過(guò)了,表示的是可以滑動(dòng)的距離。同時(shí)需要注意的是,這倆的值是固定!??!要么為12000,要么為-12000,是不是挺有意思的?
  2. 同時(shí)將計(jì)算的值更新到Action里面。Action是SmoothScroller的內(nèi)部類,主要的作用是記錄SmoothScroller滑動(dòng)需要的滑動(dòng)距離(即Dx和Dy)、滑動(dòng)時(shí)間(即time)、滑動(dòng)插值器(即mInterpolator)。快速滑動(dòng)和最后的減速滑動(dòng)就是因?yàn)檫@個(gè)插值器不同導(dǎo)致的。這里更新Action信息的操作非常的重要。

??到這里,我們應(yīng)該知道onSeekTargetStep方法干了什么事吧。我簡(jiǎn)單總結(jié)一下吧,onSeekTargetStep方法里面主要做了2件事:

  1. 更新mInterimTargetDxmInterimTargetDx,由于前面有可能滑動(dòng)了一定的距離,所以這里需要更新,這樣后面的滑動(dòng)才知道還有多少距離。
  2. 當(dāng)滑動(dòng)距離消耗完了或者是第一次調(diào)用,會(huì)調(diào)用updateActionForInterimTarget方法,重新給出新的滑動(dòng)距離,并且記錄在Action里面。

??經(jīng)過(guò)onSeekTargetStep方法之后,RecyclerView知道了新的滑動(dòng)距離之后,此時(shí)就是調(diào)用ActionrunIfNecessary方法了。我們來(lái)看看這個(gè)方法:

            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方法比較簡(jiǎn)單,就是先看看Action的信息是否被更新過(guò),如果更新過(guò),就調(diào)用smoothScrollBy方法觸發(fā)滑動(dòng);如果沒(méi)有被更新過(guò),那么什么都不做。在這里,我多說(shuō)幾句:

  1. 如果mChanged為true,即Action的信息被更新表示兩種情況:1. 這是第一次滑動(dòng);2.前面的滑動(dòng)已經(jīng)完成了,這里會(huì)觸發(fā)一次新的滑動(dòng)。mChanged設(shè)置為true,這個(gè)在前面我們已經(jīng)介紹了,就是在Action的update方法中操作的。需要的注意的是,這里的Dy就是滑動(dòng)需要的距離,如果TargetView為null的話,mDx和mDy就是為12000或者-12000;如果TargetView不為null,mDx和mDy就表示具體的距離。
  2. 如果mChanged不為true調(diào)用到這里的話,表示不需要重新觸發(fā)滑動(dòng),這是為啥呢?如果mChanged不為true,表示當(dāng)前的滑動(dòng)還未結(jié)束,即還有可滑動(dòng)的距離,此時(shí)ViewFlinger在執(zhí)行run方法時(shí),會(huì)自己調(diào)用postOnAnimation方法。這個(gè)在前面分析smoothScrollBy時(shí),我們已經(jīng)了解到了。

(2). 滑動(dòng)中

??經(jīng)過(guò)上面一小節(jié),我們知道,如果才開(kāi)始滑動(dòng)的話,滑動(dòng)距離是12000像素(這里就以正數(shù)為例)。那么接下來(lái)就是正常的滑動(dòng),正常的滑動(dòng)就如上面分析smoothScrollBy一樣,就是通過(guò)遞歸的方式從OverScroller里面獲取最新的滑動(dòng)位置,然后開(kāi)始滑動(dòng)。
??不過(guò),這里還是跟之前的分析有不同的地方,我們來(lái)看看:

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

??如果我們通過(guò)smoothScrollToPosition方法觸發(fā)了run方法的執(zhí)行,那么在每次滑動(dòng)執(zhí)行之后,都會(huì)調(diào)用onAnimation方法,來(lái)告知SmoothScroller本次滑動(dòng)了一部分的距離,進(jìn)而SmoothScroller 會(huì)更新相關(guān)的信息,執(zhí)行一些其他的操作,比如說(shuō)滑動(dòng)結(jié)束了,觸發(fā)了新的滑動(dòng),或者TargetView滑動(dòng)到屏幕中了,開(kāi)始減速滑動(dòng)。
??上面的點(diǎn)非常重要,SmoothScroller要隨時(shí)知道滑動(dòng)的狀態(tài),因?yàn)镾moothScroller可能隨時(shí)改變滑動(dòng)的策略。這個(gè)滑動(dòng)策略改變主要從滑動(dòng)結(jié)束說(shuō)起,接下來(lái)我們就看看滑動(dòng)結(jié)束的情況。

(3).滑動(dòng)結(jié)束

??一般來(lái)說(shuō),每次onAnimation的調(diào)用都有可能表示滑動(dòng)結(jié)束,那么怎么來(lái)區(qū)分它們呢?我們將滑動(dòng)結(jié)束分為兩類:

  1. 被動(dòng)結(jié)束。前面已經(jīng)說(shuō)了,smoothScrollToPosition方法一次滑動(dòng)12000像素,如果RecyclerView還沒(méi)有到我們想要的位置呢?此時(shí)調(diào)用onAnimation方法時(shí),SmoothScroller就會(huì)知道本次滑動(dòng)的滑動(dòng)距離已經(jīng)消耗完畢了,然后產(chǎn)生新的滑動(dòng)距離,也是12000像素,重新觸發(fā)一次滑動(dòng)。這個(gè)在前面分析 onSeekTargetStep方法已經(jīng)說(shuō)了,這里就不過(guò)多的分析了。這就是上面提的第一個(gè)問(wèn)題答案。
  2. 主動(dòng)結(jié)束。這種情況是ItemView已經(jīng)滑動(dòng)到屏幕中,此時(shí)調(diào)用onAnimation方法,SmoothScroller就會(huì)停止本次滑動(dòng),開(kāi)始新的一次滑動(dòng),即減速滑動(dòng)。需要注意的是,此時(shí)RecyclerView已經(jīng)知道了具體的滑動(dòng)距離,即不用調(diào)用onSeekTargetStep方法產(chǎn)生12000像素的距離。

??本小節(jié)就是重點(diǎn)分析主動(dòng)結(jié)束的情況,也就是可以尋找到上面提的第二個(gè)問(wèn)題的答案。我們直接來(lái)看看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方法中,主動(dòng)結(jié)束主要做了三件事:

  1. 調(diào)用onTargetFound方法,表示當(dāng)ItemView即將滑到屏幕中。同時(shí)從LinearSmoothScroller對(duì)onTargetFound方法的實(shí)現(xiàn),我們知道它內(nèi)部實(shí)際上對(duì)Action進(jìn)行了更新,即更新可以滑動(dòng)距離,滑動(dòng)需要的時(shí)間,以及滑動(dòng)需要的插值器(減速的插值器)。
  2. 調(diào)用runIfNecessary方法觸發(fā)一個(gè)新的滑動(dòng)。從這里,我們可以對(duì)onAnimation方法對(duì)runIfNecessary方法做一個(gè)簡(jiǎn)單的總結(jié),就是在調(diào)用runIfNecessary方法,都需要對(duì)Action內(nèi)部的信息進(jìn)行更新,只不過(guò)這里是調(diào)用onTargetFound方法,正常滑動(dòng)時(shí)調(diào)用onSeekTargetStep方法。
  3. 調(diào)用stop方法,表示當(dāng)前快速滑動(dòng)已經(jīng)結(jié)束。這里的調(diào)用能避免onAnimation方法下面的操作執(zhí)行。

??我們來(lái)看看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. 調(diào)用calculateDxToMakeVisible方法,計(jì)算可以滑動(dòng)的距離,即滑動(dòng)到目標(biāo)ItemView需要的距離。在calculateDxToMakeVisible內(nèi)部調(diào)用calculateDtToFit方法真正返回滑動(dòng)所需的距離。關(guān)于calculateDtToFit方法,后面自定義實(shí)現(xiàn)smoothScrollToPositionWithOffset方法是會(huì)使用到,這里就不過(guò)多的討論了。
  2. 調(diào)用calculateTimeForDeceleration方法,計(jì)算減速滑動(dòng)需要的時(shí)間。
  3. 調(diào)用Action的updte方法,更新相關(guān)的信息。在這里,我們傳遞了一個(gè)DecelerateInterpolator對(duì)象,這個(gè)就是減速使用的插值器。

??至此,我們就知道,RecyclerView在不知道滑動(dòng)距離的情況下,是怎么通過(guò)smoothScrollToPosition方法滑動(dòng)到具體的ItemView。待會(huì),我會(huì)做一個(gè)簡(jiǎn)單的總結(jié),在這里,我們先學(xué)以致用,實(shí)現(xiàn)一個(gè)smoothScrollToPositionWithOffset方法。

4. 實(shí)現(xiàn)smoothScrollToPositionWithOffset方法

??我們知道,不管是RecyclerView還是LayoutManger,都沒(méi)有這個(gè)方法供我們使用,那么如果我們有這個(gè)要求,自己怎么實(shí)現(xiàn)呢?其實(shí)很簡(jiǎn)單的,我們直接上代碼:

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

??其實(shí),實(shí)現(xiàn)的本質(zhì)就是通過(guò)重寫LinearSmoothScrollercalculateDtToFit方法,我們?cè)谇懊嬉呀?jīng)知道了,calculateDtToFit方法就是計(jì)算滑動(dòng)到TargetView還需要多少的距離。我們的實(shí)現(xiàn)就是在它的基礎(chǔ)加上我們想要的offset就行了,是不是很簡(jiǎn)單?
??同時(shí)SmoothScroller還是很多其他的方法,我們可以自定義或者重寫,實(shí)現(xiàn)我們想要的效果。不得不說(shuō),RecyclerView這一塊的擴(kuò)展太大了!??!

5. 總結(jié)

??到這里,本文就結(jié)束了,我在這里對(duì)本文的內(nèi)容做一個(gè)簡(jiǎn)單的總結(jié)。

  1. RecyclerView平滑滑動(dòng)提供了兩個(gè)方法:smoothScrollBysmoothScrollToPosition。其中smoothScrollBy表示滑動(dòng)具體的距離;smoothScrollToPosition表示滑動(dòng)到具體的位置。
  2. smoothScrollBy是通過(guò)遞歸實(shí)現(xiàn)的,主要依靠OverScroller完成滑動(dòng)位置的計(jì)算。
  3. smoothScrollToPosition可以分解為多個(gè)smoothScrollBy的滑動(dòng),每次滑動(dòng)12000像素。當(dāng)一次滑動(dòng)結(jié)束之后,會(huì)重新觸發(fā)一次新的12000像素的滑動(dòng);當(dāng)在某一次滑動(dòng)中,發(fā)現(xiàn)TargetView出現(xiàn)在屏幕中了,會(huì)立即停止當(dāng)前的滑動(dòng),開(kāi)始一個(gè)減速滑動(dòng)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容