上一篇介绍了手指滑动过程中,列表的滚动是如何实现的。那脱手之后,列表仍会滚动一段距离,即 fling,这又是如何实现的?走查源码一探究竟。
脱手滚动是立刻执行的吗?
写一个简单的列表 demo,用 Profiler 记录脱手滚动过程中完整的调用链(关于如何用 Profiler 找到源码执行的关键路径可以点击RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势):
脱手后会触发RecyclerView.fling()
,它的调用点在RecyclerView.onTouchEvent()
中:
public class RecyclerView { @Override public boolean onTouchEvent(MotionEvent e) { ... final MotionEvent vtev = MotionEvent.obtain(e); switch (action) { case MotionEvent.ACTION_UP: { mVelocityTracker.addMovement(vtev); // 计算速率 mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0; final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0; // 触发 fling if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { setScrollState(SCROLL_STATE_IDLE); } resetScroll(); } break; } ... return true; } }
ACTION_UP
事件发生时,将VelocityTracker
计算得出的速率传入RecyclerView.fling()
来触发 fling:
public class RecyclerView { public boolean fling(int velocityX, int velocityY) { ... // 若滚动速率小于阈值 则直接 return 不触发 fling if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) { velocityX = 0; } if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) { velocityY = 0; } if (velocityX == 0 && velocityY == 0) { return false; } // 让父控件提前消费 fling if (!dispatchNestedPreFling(velocityX, velocityY)) { ... velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity)); velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity)); // 委托给 ViewFlinger 触发 fling mViewFlinger.fling(velocityX, velocityY); return true; } } return false; } }
RecyclerView 在触发 fling 之前,处理了嵌套滚动逻辑,即让父控件提前消费 fling,若父控件未消费,则将 fling 委托给ViewFlinger
:
public class RecyclerView { class ViewFlinger implements Runnable { OverScroller mOverScroller; public void fling(int velocityX, int velocityY) { ... // 计算 fling 相关的数值 mOverScroller.fling(0, 0, velocityX, velocityY,Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); // 将 ViewFlinger 作为动画抛出 postOnAnimation(); } void postOnAnimation() { ... // 将 ViewFlinger 作为动画抛出 internalPostOnAnimation(); } private void internalPostOnAnimation() { removeCallbacks(this); // 将 ViewFlinger post 到主线程消息队列 ViewCompat.postOnAnimation(RecyclerView.this, this); } } }
ViewFlinger
是一个 Runnable。列表脱手滚动逻辑也被包装在这个 Runnable 中并 post 到主线消息队列中:
public class ViewCompat { public static void postOnAnimation(@NonNull View view, Runnable action) { if (Build.VERSION.SDK_INT >= 16) { // 将抛消息委托给 view view.postOnAnimation(action); } else { view.postDelayed(action, ValueAnimator.getFrameDelay()); } } } public class View { public void postOnAnimation(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { // 将抛消息委托给 Choreographer 并将消息类型指定为 CALLBACK_ANIMATION attachInfo.mViewRootImpl.mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, action, null); } else { getRunQueue().post(action); } } }
Choreographer
是用来将上层绘制任务和底层垂直同步信号进行协调的一个类:
public final class Choreographer { // 输入任务 public static final int CALLBACK_INPUT = 0; // 动画任务 public static final int CALLBACK_ANIMATION = 1; // view树遍历任务 public static final int CALLBACK_TRAVERSAL = 2; // COMMIT任务 public static final int CALLBACK_COMMIT = 3; // 暂存任务的链式数组 private final CallbackQueue[] mCallbackQueues; // 主线程消息处理器 private final FrameHandler mHandler; // 抛绘制任务 public void postCallback(int callbackType, Runnable action, Object token) { postCallbackDelayed(callbackType, action, token, 0); } // 延迟抛绘制任务 public void postCallbackDelayed(int callbackType,Runnable action, Object token, long delayMillis) { ... postCallbackDelayedInternal(callbackType, action, token, delayMillis); } // 抛绘制任务的具体实现 private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) { synchronized (mLock) { final long now = SystemClock.uptimeMillis(); final long dueTime = now + delayMillis; // 1. 将绘制任务根据类型暂存在链式结构中 mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); // 2. 订阅下一个垂直同步信号 if (dueTime <= now) { // 立刻订阅下一个垂直同步信号 scheduleFrameLocked(now); } else { // 在未来的某个时间点订阅垂直同步信号 Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); msg.arg1 = callbackType; msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, dueTime); } } } // 主线程消息处理器 private final class FrameHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { ... case MSG_DO_SCHEDULE_CALLBACK: // 在未来时间点订阅垂直同步信号 doScheduleCallback(msg.arg1); break; } } } }
Choreographer
内部维护了一个链式数组结构CallbackQueue[]
,数组中存放四中类型的任务,分别是输入任务、动画任务、view树遍历任务、COMMIT任务。
这些任务在下一个垂直同步信号到来之时,会被取出执行。
public final class Choreographer { // 垂直同步信号接收器 private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { // 垂直同步信号到来 @Override public void onVsync(long timestampNanos, int builtInDisplayId, int frame) { ... // 发送异步消息到主线程,执行当前的 Runnable,即 doFrame() Message msg = Message.obtain(mHandler, this); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); } @Override public void run() { mHavePendingVsync = false; // 绘制一帧的内容 doFrame(mTimestampNanos, mFrame); } } }
其中doFrome()
表示绘制垂直信号到来的当前帧:
public final class Choreographer { void doFrame(long frameTimeNanos, int frame) { final long startNanos; synchronized (mLock) { ... try { // 处理这一帧的输入事件 doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); // 处理这一帧的动画 doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); // 处理这一帧的 View 树遍历 doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); // 所有绘制任务结束后执行 COMMIT 任务 doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos); } } }
绘制当前帧会按序处理之前暂存的四种任务。更详细的Choreographer
解析可以点击读源码长知识 | Android卡顿真的是因为”掉帧“?
至此可以得出结论:
RecyclerView 的脱手滚动不是立刻执行的,触发脱手滚动时,一个叫
ViewFlinger
的Runnable
会被抛到Choreographer
中,它被包装成一个动画任务。等待下一个垂直同步信号到来时,这个任务就在主线程被执行。
连续执行脱手滚动任务
这个被抛到主线程执行的任务就在ViewFlinger.run()
中:
public class RecyclerView { class ViewFlinger implements Runnable { @Override public void run() { ... final OverScroller scroller = mOverScroller; // OverScroller 计算滚动位置 返回 true 表示滚动还未完成 if (scroller.computeScrollOffset()) { final int x = scroller.getCurrX(); final int y = scroller.getCurrY(); int unconsumedX = x - mLastFlingX; int unconsumedY = y - mLastFlingY; mLastFlingX = x; mLastFlingY = y; int consumedX = 0; int consumedY = 0; // 回调嵌套滚动,以让 RecyclerView 的父控件优先消费滚动距离 if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null, TYPE_NON_TOUCH)) { unconsumedX -= mReusableIntPair[0]; unconsumedY -= mReusableIntPair[1]; } ... if (mAdapter != null) { mReusableIntPair[0] = 0; mReusableIntPair[1] = 0; // 列表自己滚动一小段,将嵌套滚动的剩余值给 RecyclerView 消费 scrollStep(unconsumedX, unconsumedY, mReusableIntPair); consumedX = mReusableIntPair[0]; consumedY = mReusableIntPair[1]; unconsumedX -= consumedX; unconsumedY -= consumedY; ... } ... // 判断当前滚动位置和目标滚动位置是否相等 boolean scrollerFinishedX = scroller.getCurrX() == scroller.getFinalX(); boolean scrollerFinishedY = scroller.getCurrY() == scroller.getFinalY(); // 通过 OverScroller 判断滚动是否结束 final boolean doneScrolling = scroller.isFinished() || ((scrollerFinishedX || unconsumedX != 0) && (scrollerFinishedY || unconsumedY != 0)); SmoothScroller smoothScroller = mLayout.mSmoothScroller; boolean smoothScrollerPending = smoothScroller != null && smoothScroller.isPendingInitialRun(); if (!smoothScrollerPending && doneScrolling) { ... } else { // 如果滚动还未结束,则继续将自己(ViewFlinger)抛到主线程执行 postOnAnimation(); if (mGapWorker != null) { mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY); } } } ... } } }
RecyclerView 的脱手滚动是一小段一小段进行的。每一小段的滚动通过RecyclerView.scrollStep()
落实。在RecyclerView 的滚动是怎么实现的?| 解锁阅读源码新姿势中已经介绍过,它会根据滚动距离在滚动方向上填充额外的表项,然后将所有表项向滚动的反方向平移相同距离,以实现滚动。
滚完了一小段,是否还要滚下一段?这是由OverScroller
说了算的,如果当前滚动位置和目标滚动位置不相等,则表示滚动还未结束,此时会再次执行postOnAnimation()
将ViewFlinger
这个 Runnable 抛到主线程执行。如此往复列表就自己滚起来了。
OverScroller
用于存储并计算所有和滚动相关的数值:
public class OverScroller { // 横向 Scroller private final SplineOverScroller mScrollerX; // 纵向 Scroller private final SplineOverScroller mScrollerY; static class SplineOverScroller { // 初始滚动位置 private int mStart; // 当前滚动位置 private int mCurrentPosition; // 目标滚动位置 private int mFinal; // 滚动速率 private int mVelocity; // 滚动开始时间 private long mStartTime; // 滚动时长 private int mDuration; ... } }
在每个ViewFlinger.run()
被执行的开头,通过OverScroll.computeScrollOffset()
触发更新这次要滚的那一小段位移值:
public class OverScroller { public boolean computeScrollOffset() { if (isFinished()) { return false; } switch (mMode) { // 跟手滚动时位移计算算法 case SCROLL_MODE: long time = AnimationUtils.currentAnimationTimeMillis(); final long elapsedTime = time - mScrollerX.mStartTime; final int duration = mScrollerX.mDuration; if (elapsedTime < duration) { final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration); mScrollerX.updateScroll(q); mScrollerY.updateScroll(q); } else { abortAnimation(); } break; // 脱手滚动时位移计算算法 case FLING_MODE: // 横向 if (!mScrollerX.mFinished) { if (!mScrollerX.update()) { if (!mScrollerX.continueWhenFinished()) { mScrollerX.finish(); } } } // 纵向 if (!mScrollerY.mFinished) { if (!mScrollerY.update()) { if (!mScrollerY.continueWhenFinished()) { mScrollerY.finish(); } } } break; } return true; } }
对于 fling 来说,计算滚动位移的算法在update()
方法中:
public class OverScroller { static class SplineOverScroller { // 根据一定算法更新当前位置 boolean update() { final long time = AnimationUtils.currentAnimationTimeMillis(); final long currentTime = time - mStartTime; if (currentTime == 0) { return mDuration > 0; } if (currentTime > mDuration) { return false; } double distance = 0.0; // 根据不同的状态选择不同算法计算滚动速率及当前滚动位置 switch (mState) { case SPLINE: { final float t = (float) currentTime / mSplineDuration; final int index = (int) (NB_SAMPLES * t); float distanceCoef = 1.f; float velocityCoef = 0.f; if (index < NB_SAMPLES) { final float t_inf = (float) index / NB_SAMPLES; final float t_sup = (float) (index + 1) / NB_SAMPLES; final float d_inf = SPLINE_POSITION[index]; final float d_sup = SPLINE_POSITION[index + 1]; velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); distanceCoef = d_inf + (t - t_inf) * velocityCoef; } distance = distanceCoef * mSplineDistance; mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f; break; } case BALLISTIC: { final float t = currentTime / 1000.0f; mCurrVelocity = mVelocity + mDeceleration * t; distance = mVelocity * t + mDeceleration * t * t / 2.0f; break; } case CUBIC: { final float t = (float) (currentTime) / mDuration; final float t2 = t * t; final float sign = Math.signum(mVelocity); distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2); mCurrVelocity = sign * mOver * 6.0f * (- t + t2); break; } } //更新当前滚动位置 mCurrentPosition = mStart + (int) Math.round(distance); return true; } } }
算法很复杂,看也看不懂(猜测手机厂商会优化这块以实现更顺滑的手感),不过也不影响今天探讨的主题。待以后有需求时再深挖。
算法的终点就是更新当前滚动位置mCurrentPosition
的值。
总结
- 对于RecyclerView,不管是跟手还是脱手滚动,最终滚动的落实都是通过调用
View.offsetTopAndBottom()
向滚动的反方向平移表项实现的。
OverScroller
不仅存储并计算了和滚动相关的所有数值。RecyclerView 还借助于它来控制滚动是否要继续。
- RecyclerView 的脱手滚动(fling)是一段一段进行的,每一小段的滚动都被包裹在一个叫
ViewFlinger
的 Runnable 中。它会被抛到Choreographer
中,作为动画任务暂存起来。待一下个垂直同步信号到来之时,被抛到主线程的消息队列中执行。
- 只要
OverScroller
判断滚动尚未结束,ViewFlinger
会重复上述过程,即再一次把自己抛到主线程中执行。如此往复,列表就脱手滚了起来。
推荐阅读
RecyclerView 系列文章目录如下: