20.LinearSmoothScroller源码分析(26.0.0)-控制RecyclerView滑动速度

简介: smoothScrollToPosition方法可以使RecyclerView滑动到指定的位置,来看下源码 /** * Starts a smooth scroll to an adapter position.

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);
    }
}
相关文章
|
Android开发
【RecyclerView】 五、RecyclerView 布局 ( 瀑布流 | 交错网格局管理器 StaggeredGridLayoutManager )
【RecyclerView】 五、RecyclerView 布局 ( 瀑布流 | 交错网格局管理器 StaggeredGridLayoutManager )
675 0
【RecyclerView】 五、RecyclerView 布局 ( 瀑布流 | 交错网格局管理器 StaggeredGridLayoutManager )
|
3月前
|
UED
Flutter之ListView实现自动滑动到底部
Flutter之ListView实现自动滑动到底部
216 1
|
存储 消息中间件 缓存
RecyclerView 的滚动时怎么实现的?(二)| Fling
RecyclerView 的滚动时怎么实现的?(二)| Fling
199 0
RecyclerView#smoothScrollToPosition调用RecyclerView#OnScrollListener的过程
项目中使用到了RecyclerView#smoothScrollToPosition(0)方法让Recyclerview滚动到顶部,同时给Recyclerview设置了监听器RecyclerView.OnScrollListener。
|
存储 缓存 索引
更好的 RecyclerView 表项子控件点击监听器
上篇介绍了一种新的监听 RecyclerView 表项点击事件的方法。实现了将点击事件和RecyclerView.Adapter解耦。这一篇介绍如何监听 RecyclerView 表项子控件点击事件。
590 0
|
存储 缓存 算法
读源码长知识 | 更好的 RecyclerView 表项点击监听器
RecyclerView没有提供表项点击事件监听器,只能自己处理。这一篇介绍一种更加解耦,更易于使用的表项点击事件监听方法。
213 0
|
开发者 异构计算
RecyclerView滑动时卡顿怎么办?
RecyclerView滑动时卡顿怎么办?
573 0
RecyclerView滑动时卡顿怎么办?
|
缓存 算法
深入讲解RecyclerView布局动画原理(一)
深入讲解RecyclerView布局动画原理(一)
深入讲解RecyclerView布局动画原理(一)
深入讲解RecyclerView布局动画原理(二)
深入讲解RecyclerView布局动画原理(二)
深入讲解RecyclerView布局动画原理(二)
|
API
为RecyclerView添加下拉刷新功能
在之前的文章中,我们实现了带有header和footer功能的WrapRecyclerView。 现今App中列表的下拉刷新和上拉加载已经是一种习惯了,这两个操作也确实方便很多。 为RecyclerView添加这个功能可以通过多种方法,这里我选用了一种简单的做法。基于pulltorefresh这个库。
187 0