smoothScrollToPosition方法可以使RecyclerView滑动到指定的位置,来看下源码
/**
* Starts a smooth scroll to an adapter position.
* <p>
* To support smooth scrolling, you must override
* {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} and create a
* {@link SmoothScroller}.
* <p>
* {@link LayoutManager} is responsible for creating the actual scroll action. If you want to
* provide a custom smooth scroll logic, override
* {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} in your
* LayoutManager.
*
* @param position The adapter position to scroll to
* @see LayoutManager#smoothScrollToPosition(RecyclerView, State, int)
*/
public void smoothScrollToPosition(int position) {
if (mLayoutFrozen) {
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);
}
代码中的mLayout指的就是RecyclerView的layoutManager,所以RecyclerView的滑动其实是在LayoutMangare中实现的。如注释所说,LayoutManager is responsible for creating the actual scroll action. If you want to provide a custom smooth scroll logic, override
link LayoutManager#smoothScrollToPosition(RecyclerView, State, int) in your LayoutManager.如果我们要实现自定义的smooth scroll,只要自定义一个LayoutManager,重写
smoothScrollToPosition(RecyclerView, State, int)这个方法即可。
那么在此之前我们有必要去看看官方的smooth scroll是如何实现的,以LinearLayoutManager为例,我们去看看它的smoothScrollToPosition方法.这里定义了一个LinearSmoothScroller ,通过startSmoothScroll将它传入,接着往下走
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
LinearSmoothScroller linearSmoothScroller =
new LinearSmoothScroller(recyclerView.getContext());
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
/**
* <p>Starts a smooth scroll using the provided SmoothScroller.</p>
* <p>Calling this method will cancel any previous smooth scroll request.</p>
* @param smoothScroller Instance which defines how smooth scroll should be animated
*/
public void startSmoothScroll(SmoothScroller smoothScroller) {
if (mSmoothScroller != null && smoothScroller != mSmoothScroller
&& mSmoothScroller.isRunning()) {
mSmoothScroller.stop();
}
mSmoothScroller = smoothScroller;
//将RecylerView传递到SmoothScroller中开启滑动
mSmoothScroller.start(mRecyclerView, this);
}
/**
* Starts a smooth scroll for the given target position.
* <p>In each animation step, {@link RecyclerView} will check
* for the target view and call either
* {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
* {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until
* SmoothScroller is stopped.</p>
*
* <p>Note that if RecyclerView finds the target view, it will automatically stop the
* SmoothScroller. This <b>does not</b> mean that scroll will stop, it only means it will
* stop calling SmoothScroller in each animation step.</p>
*/
void start(RecyclerView recyclerView, LayoutManager layoutManager) {
mRecyclerView = recyclerView;
mLayoutManager = layoutManager;
if (mTargetPosition == RecyclerView.NO_POSITION) {
throw new IllegalArgumentException("Invalid target position");
}
mRecyclerView.mState.mTargetPosition = mTargetPosition;
mRunning = true;
mPendingInitialRun = true;
//获取到指定position的view
mTargetView = findViewByPosition(getTargetPosition());
onStart();
//mViewFlinger是RecyclerView的一个内部类,实现了Runnable接口,它是对OverScrolloer的一个包装用于操作滑动
//当下边的代码执行的时候,会调用其run方法
mRecyclerView.mViewFlinger.postOnAnimation();
}
//看下ViewCompat.postOnAnimation方法,最终它会走到view.postDelayed(action, getFrameTime());将
//Runnable加入队列执行
void postOnAnimation() {
if (mEatRunOnAnimationRequest) {
mReSchedulePostAnimationCallback = true;
} else {
removeCallbacks(this);
ViewCompat.postOnAnimation(RecyclerView.this, this);
}
}
通过上边的代码可以看出,最终RecyclerView的滑动是在SmoothScroller中实现的,SmoothScroller是一个抽象类,LinearSmoothScroller是它的子类。mSmoothScroller.start(mRecyclerView, this);是开启滑动的入口。可以说SmoothScroller是对RecyclerView滑动事件的包装类,用于控制滑动的逻辑,那么我们可以猜想,既然SmoothScroller包装了RecyclerView的滑动逻辑,那么应该在这个类中可以找到控制RecyclerView滑动速度的代码。所以这就是今天要分析inearSmoothScroller源码的原因。
先看看run方法中执行的逻辑
@Override
public void run() {
......
if (scroller.computeScrollOffset()) {
......
if (mAdapter != null) {
......
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(dx - overscrollX, dy - overscrollY);
} else {
smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
}
}
}
......
// call this after the onAnimation is complete not to have inconsistent callbacks etc.
if (smoothScroller != null) {
if (smoothScroller.isPendingInitialRun()) {
smoothScroller.onAnimation(0, 0);
}
if (!mReSchedulePostAnimationCallback) {
smoothScroller.stop(); //stop if it does not trigger any scroll
}
}
enableRunOnAnimationRequests();
}
run方法执行,会调用SmoothScroller的onAnimation方法
private void onAnimation(int dx, int dy) {
final RecyclerView recyclerView = mRecyclerView;
if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
stop();
}
mPendingInitialRun = false;
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;
}
}
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();
} else {
stop(); // done
}
}
}
}
最终在下面的方法中执行滑动的效果
void runIfNecessary(RecyclerView recyclerView) {
if (mJumpToPosition >= 0) {
final int position = mJumpToPosition;
mJumpToPosition = NO_POSITION;
recyclerView.jumpToPositionForSmoothScroller(position);
mChanged = false;
return;
}
if (mChanged) {
validate();
if (mInterpolator == null) {
if (mDuration == UNDEFINED_DURATION) {
recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy);
} else {
recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration);
}
} else {
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;
}
}
recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration);这行代码是最终的滑动执行,注意onTargetFound和onSeekTargetStep两个方法,这两个方法都是LinearSmoothScroller中重写的方法,正如最初注释所说,这两个方法会被不断的调用,直到SmoothScroller停止执行
* Starts a smooth scroll for the given target position.
* <p>In each animation step, {@link RecyclerView} will check
* for the target view and call either
* {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
* {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until
* SmoothScroller is stopped.</p>
接下来看看LinearSmoothScroller的源码,在构造方法中计算了一个值,从方法名就可以看出,这个值表示的是view每个像素的滑动速度
public LinearSmoothScroller(Context context) {
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
}
这个速度值是这样计算的,那么看到这里我们大概也可以猜到重写这个方法,返回不同的速度值岂不是就可以控制view的滑动速度了,的确如此
/**
* Calculates the scroll speed.
*
* @param displayMetrics DisplayMetrics to be used for real dimension calculations
* @return The time (in ms) it should take for each pixel. For instance, if returned value is
* 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
*/
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
接下来再看看上边提到的onTargetFound onSeekTargetStep两个方法
/**
* {@inheritDoc}
* 计算需要滑动的距离和时间,更新到action上
*/
@Override
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);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
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
}
计算减速的时间,用于控制移动视觉效果的平滑性
/**
* <p>Calculates the time for deceleration so that transition from LinearInterpolator to
* DecelerateInterpolator looks smooth.</p>
*
* @param dx Distance to scroll
* @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
* from LinearInterpolation
*/
protected int calculateTimeForDeceleration(int dx) {
// we want to cover same area with the linear interpolator for the first 10% of the
// interpolation. After that, deceleration will take control.
// area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
// which gives 0.100028 when x = .3356
// this is why we divide linear scrolling time with .3356
return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
}
计算滑动需要的总时间
/**
* Calculates the time it should take to scroll the given distance (in pixels)
*
* @param dx Distance in pixels that we want to scroll
* @return Time in milliseconds
* @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
*/
protected int calculateTimeForScrolling(int dx) {
// In a case where dx is very small, rounding may return 0 although dx > 0.
// To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
// time.
return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
}
总结一下
那么回到我们最初的目标,我们是想知道,在调用smoothScrollToPosition的时候,如何控制RecyclerView的滑动速度,看过源码就很简单了,首先自定义一个Layoutmanager,假设继承自LinearLayoutManager,重写他的public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) 方法,然后自定义一个LinearSmoothScroller继承自LinearSmoothScroller,重写calculateSpeedPerPixel方法用于控制每像素移动的速度从而达到控制RecyclerView滑动速度的目的
public class FastScrollLinearLayoutManager extends LinearLayoutManager {
public FastScrollLinearLayoutManager(Context context) {
super(context);
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()){
//返回你要设置的速度值
return super.calculateSpeedPerPixel(displayMetrics);
}
};
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
}