时隔一年多,我又来更新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
方法,这里我们就看一下LinearLayoutManager
的smoothScrollToPosition
吧(其实StaggeredGridLayoutManager
和LinearLayoutManager
的实现是一样的)。
@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方法。那么是怎么触发滑动的呢?答案就在下面调用的SmoothScroller
的 onAnimation
方法。从前面的分析,我们知道,我们通过调用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
对象已经停止滑动,这个对象就是我们在LinearLayoutManager
的smoothScrollToPosition
方法创建的对象。大家应该可以从我的描述中得到一些信息,没错,减速滑动是通过另一个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表示本次滑动消耗的距离,mInterimTargetDx
和mInterimTargetDy
表示一共需要滑动的距离。因为我们这里是第一次调用onSeekTargetStep
方法,也就是说dy为0,同时mInterimTargetDx
和mInterimTargetDy
也为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
方法看上去挺复杂的,但是实际上就是做了两件事:
- 计算
mInterimTargetDx
和mInterimTargetDy
,以滑动时间的time。这两个变量,我们前面已经见过了,表示的是可以滑动的距离。同时需要注意的是,这俩的值是固定!!!要么为12000,要么为-12000,是不是挺有意思的?- 同时将计算的值更新到
Action
里面。Action是SmoothScroller
的内部类,主要的作用是记录SmoothScroller
滑动需要的滑动距离(即Dx和Dy)、滑动时间(即time)、滑动插值器(即mInterpolator
)。快速滑动和最后的减速滑动就是因为这个插值器不同导致的。这里更新Action信息的操作非常的重要。
到这里,我们应该知道onSeekTargetStep
方法干了什么事吧。我简单总结一下吧,onSeekTargetStep方法里面主要做了2件事:
- 更新
mInterimTargetDx
和mInterimTargetDx
,由于前面有可能滑动了一定的距离,所以这里需要更新,这样后面的滑动才知道还有多少距离。- 当滑动距离消耗完了或者是第一次调用,会调用
updateActionForInterimTarget
方法,重新给出新的滑动距离,并且记录在Action里面。
经过onSeekTargetStep
方法之后,RecyclerView知道了新的滑动距离之后,此时就是调用Action
的runIfNecessary
方法了。我们来看看这个方法:
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即将滑到屏幕中。同时从LinearSmoothScroller
对onTargetFound
方法的实现,我们知道它内部实际上对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)
}
}
其实,实现的本质就是通过重写LinearSmoothScroller
的calculateDtToFit
方法,我们在前面已经知道了,calculateDtToFit
方法就是计算滑动到TargetView还需要多少的距离。我们的实现就是在它的基础加上我们想要的offset就行了,是不是很简单?
同时SmoothScroller
还是很多其他的方法,我们可以自定义或者重写,实现我们想要的效果。不得不说,RecyclerView这一块的扩展太大了!!!
5. 总结
到这里,本文就结束了,我在这里对本文的内容做一个简单的总结。
- RecyclerView平滑滑动提供了两个方法:
smoothScrollBy
和smoothScrollToPosition
。其中smoothScrollBy
表示滑动具体的距离;smoothScrollToPosition
表示滑动到具体的位置。smoothScrollBy
是通过递归实现的,主要依靠OverScroller完成滑动位置的计算。smoothScrollToPosition
可以分解为多个smoothScrollBy
的滑动,每次滑动12000像素。当一次滑动结束之后,会重新触发一次新的12000像素的滑动;当在某一次滑动中,发现TargetView出现在屏幕中了,会立即停止当前的滑动,开始一个减速滑动。