RecyclerView 的滚动时怎么实现的?(二)| Fling

简介: 上一篇介绍了手指滑动过程中,列表的滚动是如何实现的。那脱手之后,列表仍会滚动一段距离,即 fling,这又是如何实现的?走查源码一探究竟。

[上一篇]()介绍了手指滑动过程中,列表的滚动是如何实现的。那脱手之后,列表仍会滚动一段距离,即 fling,这又是如何实现的?走查源码一探究竟。

脱手滚动是立刻执行的吗?

写一个简单的列表 demo,用 Profiler 记录脱手滚动过程中完整的调用链(关于如何用 Profiler 找到源码执行的关键路径可以点击RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势):

微信截图_20210508115150.png

脱手后会触发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 的脱手滚动不是立刻执行的,触发脱手滚动时,一个叫 ViewFlingerRunnable会被抛到 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 系列文章目录如下:

RecyclerView 系列文章目录如下:

  1. RecyclerView 缓存机制 | 如何复用表项?
  2. RecyclerView 缓存机制 | 回收些什么?
  3. RecyclerView 缓存机制 | 回收到哪去?
  4. RecyclerView缓存机制 | scrap view 的生命周期
  5. 读源码长知识 | 更好的RecyclerView点击监听器
  6. 代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂
  7. 更好的 RecyclerView 表项子控件点击监听器
  8. 更高效地刷新 RecyclerView | DiffUtil二次封装
  9. 换一个思路,超简单的RecyclerView预加载
  10. RecyclerView 动画原理 | 换个姿势看源码(pre-layout)
  11. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系
  12. RecyclerView 动画原理 | 如何存储并应用动画属性值?
  13. RecyclerView 面试题 | 列表滚动时,表项是如何被填充或回收的?
  14. RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?
  15. RecyclerView 性能优化 | 把加载表项耗时减半 (一)
  16. RecyclerView 性能优化 | 把加载表项耗时减半 (二)
  17. RecyclerView 性能优化 | 把加载表项耗时减半 (三)
  18. RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势
  19. RecyclerView 的滚动时怎么实现的?(二)| Fling
  20. RecyclerView 刷新列表数据的 notifyDataSetChanged() 为什么是昂贵的?
目录
相关文章
|
7月前
|
Android开发
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X6 webview(二)
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X6 webview
|
7月前
|
Android开发
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X6 webview(一)
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X6 webview
|
7月前
|
Android开发
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
|
9月前
不用涉及到各种冲突常规打造酷炫下拉视差效果SmartRefreshLayout+ViewPager+RecyclerView
不用涉及到各种冲突常规打造酷炫下拉视差效果SmartRefreshLayout+ViewPager+RecyclerView
125 0
|
11月前
|
存储 消息中间件 缓存
RecyclerView 的滚动时怎么实现的?(二)| Fling
RecyclerView 的滚动时怎么实现的?(二)| Fling
130 0
|
11月前
|
消息中间件 存储 缓存
RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势
RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势
95 0
|
Android开发
Android开发 ListView(垂直滚动列表项视图)的简单使用
Android开发 ListView(垂直滚动列表项视图)的简单使用
307 0
Android开发 ListView(垂直滚动列表项视图)的简单使用
老大爷都能看懂的RecyclerView动画原理
老大爷都能看懂的RecyclerView动画原理
老大爷都能看懂的RecyclerView动画原理
老大爷都能看懂的RecyclerView动画原理之二
老大爷都能看懂的RecyclerView动画原理之二
老大爷都能看懂的RecyclerView动画原理之二