??時(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主要分為兩部分:smoothScrollBy和smoothScrollToPosition。其中,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步:
- 首先通過(guò)調(diào)用OvserScroller的
computeScrollOffset方法來(lái)判斷還有可以滑動(dòng)的距離。如果可以滑動(dòng)的距離,那么computeScrollOffset方法返回的true,此時(shí)我們可以通過(guò)getCurrX方法或者getCurrY方法獲取最新的滑動(dòng)位置。- 處理滑動(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é)可以看看。- 判斷是否滑動(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)題:
RecyclerView是怎么通過(guò)遞歸方式滑動(dòng)到指定位置的?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方法,這里我們就看一下LinearLayoutManager的smoothScrollToPosition吧(其實(shí)StaggeredGridLayoutManager和LinearLayoutManager的實(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方法一共做了三件事:
- 如果之前已經(jīng)在滑動(dòng)了,會(huì)將它停止。
- 將新的
SmoothScroller對(duì)象賦值給mSmoothScroller。大家要記得這一步操作,因?yàn)楹竺娴膬?nèi)容我們經(jīng)??匆?jiàn)它。- 調(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)用的SmoothScroller的 onAnimation方法。從前面的分析,我們知道,我們通過(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方法里面主要分為三步,如上面的注釋,我們分別看一下:
- 如果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)
- 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方法里面。- 如果當(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)消耗的距離,mInterimTargetDx和mInterimTargetDy表示一共需要滑動(dòng)的距離。因?yàn)槲覀冞@里是第一次調(diào)用onSeekTargetStep方法,也就是說(shuō)dy為0,同時(shí)mInterimTargetDx和mInterimTargetDy也為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í)際上就是做了兩件事:
- 計(jì)算
mInterimTargetDx和mInterimTargetDy,以滑動(dòng)時(shí)間的time。這兩個(gè)變量,我們前面已經(jīng)見(jiàn)過(guò)了,表示的是可以滑動(dòng)的距離。同時(shí)需要注意的是,這倆的值是固定!??!要么為12000,要么為-12000,是不是挺有意思的?- 同時(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件事:
- 更新
mInterimTargetDx和mInterimTargetDx,由于前面有可能滑動(dòng)了一定的距離,所以這里需要更新,這樣后面的滑動(dòng)才知道還有多少距離。- 當(dāng)滑動(dòng)距離消耗完了或者是第一次調(diào)用,會(huì)調(diào)用
updateActionForInterimTarget方法,重新給出新的滑動(dòng)距離,并且記錄在Action里面。
??經(jīng)過(guò)onSeekTargetStep方法之后,RecyclerView知道了新的滑動(dòng)距離之后,此時(shí)就是調(diào)用Action的runIfNecessary方法了。我們來(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ō)幾句:
- 如果
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就表示具體的距離。- 如果
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é)束分為兩類:
- 被動(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)題答案。- 主動(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é)束主要做了三件事:
- 調(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)需要的插值器(減速的插值器)。- 調(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方法。- 調(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件事:
- 調(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ò)多的討論了。- 調(diào)用
calculateTimeForDeceleration方法,計(jì)算減速滑動(dòng)需要的時(shí)間。- 調(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ò)重寫LinearSmoothScroller的calculateDtToFit方法,我們?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é)。
- RecyclerView平滑滑動(dòng)提供了兩個(gè)方法:
smoothScrollBy和smoothScrollToPosition。其中smoothScrollBy表示滑動(dòng)具體的距離;smoothScrollToPosition表示滑動(dòng)到具體的位置。smoothScrollBy是通過(guò)遞歸實(shí)現(xiàn)的,主要依靠OverScroller完成滑動(dòng)位置的計(jì)算。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)。