如果需要实现RecyclerView滚动到指定目标的位置,简单的说明下:

#1. RecyclerView.scrollToPosition(int)

/**
     * Convenience method to scroll to a certain position.
     *
     * RecyclerView does not implement scrolling logic, rather forwards the call to
     * {@link RecyclerView.LayoutManager#scrollToPosition(int)}
     * @param position Scroll to this adapter position
     * @see RecyclerView.LayoutManager#scrollToPosition(int)
     */
    public void scrollToPosition(int position) {
        if (mLayoutSuppressed) {
            return;
        }
        stopScroll();
        if (mLayout == null) {
            Log.e(TAG, "Cannot scroll to position a LayoutManager set. "
                    + "Call setLayoutManager with a non-null argument.");
            return;
        }
        mLayout.scrollToPosition(position);
        awakenScrollBars();
    }

注解:滚动到某个位置的便捷方法。 RecyclerView不实现滚动逻辑,而是将调用转发到RecyclerView.LayoutManager.scrollToPosition(int),所以我们也可以理解为LayoutManager.scrollToPosition(int)

通过这个方法,我们可以使目标Item滚动到当前可视位置,而它不会将目标Item刻意的滚动到顶部第一个可见位置,或者底部的第后一个可见位置,这里只会将目标Item可见,我们也看下代码:
#LayoutManager.scrollToPosition(int)

/**
     * <p>Scroll the RecyclerView to make the position visible.</p>
     *
     * <p>RecyclerView will scroll the minimum amount that is necessary to make the
     * target position visible. If you are looking for a similar behavior to
     * {@link android.widget.ListView#setSelection(int)} or
     * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use
     * {@link #scrollToPositionWithOffset(int, int)}.</p>
     *
     * <p>Note that scroll position change will not be reflected until the next layout call.</p>
     *
     * @param position Scroll to this adapter position
     * @see #scrollToPositionWithOffset(int, int)
     */
    @Override
    public void scrollToPosition(int position) {
        mPendingScrollPosition = position;
        mPendingScrollPositionOffset = INVALID_OFFSET;
        if (mPendingSavedState != null) {
            mPendingSavedState.invalidateAnchor();
        }
        requestLayout();
    }

* Scroll the RecyclerView to make the position visible. => 滚动RecyclerView以使该位置可见。

 

#2. RecyclerView/LayoutManager.scrollToPositionWithOffset(int, offset)

/**
     * Scroll to the specified adapter position with the given offset from resolved layout
     * start. Resolved layout start depends on {@link #getReverseLayout()},
     * {@link ViewCompat#getLayoutDirection(android.view.View)} and {@link #getStackFromEnd()}.
     * <p>
     * For example, if layout is {@link #VERTICAL} and {@link #getStackFromEnd()} is true, calling
     * <code>scrollToPositionWithOffset(10, 20)</code> will layout such that
     * <code>item[10]</code>'s bottom is 20 pixels above the RecyclerView's bottom.
     * <p>
     * Note that scroll position change will not be reflected until the next layout call.
     * <p>
     * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}.
     *
     * @param position Index (starting at 0) of the reference item.
     * @param offset   The distance (in pixels) between the start edge of the item view and
     *                 start edge of the RecyclerView.
     * @see #setReverseLayout(boolean)
     * @see #scrollToPosition(int)
     */
    public void scrollToPositionWithOffset(int position, int offset) {
        mPendingScrollPosition = position;
        mPendingScrollPositionOffset = offset;
        if (mPendingSavedState != null) {
            mPendingSavedState.invalidateAnchor();
        }
        requestLayout();
    }

简单的说下:offset - 项目视图的起始边缘与RecyclerView的起始边缘之间的距离(以像素为单位)。这里相比scrollToPosition,我们就可以设置偏移量:

如果offset = 0,我们可以理解为将目标Item刻意的滚动到顶部第一个可见位置,如果offset = 100,将目标Item刻意的滚动到距离顶部第一个可见位置往下偏移100px,然后以此类推...

如果只是想使某个位置可见,请使用scrollToPosition(int)

 

 #3. RecyclerView.smoothScrollBy(dx, dy)

/**
     * Animate a scroll by the given amount of pixels along either axis.
     *
     * @param dx Pixels to scroll horizontally
     * @param dy Pixels to scroll vertically
     */
    public void smoothScrollBy(@Px int dx, @Px int dy) {
        smoothScrollBy(dx, dy, null);
    }

这里smoothScrollBy是有一系列的方法,有丰富的参数来展示不同的滚动效果:

@Px int dx, // dx像素水平滚动
@Px int dy, // dy像素垂直滚动
@Nullable Interpolator interpolator, // 用于滚动的插值器。 如果为null,则RecyclerView将使用内部默认插值器。
int duration, // 动画的持续时间(以毫秒为单位)。 设置为UNDEFINED_DURATION可以根据内部定义的标准初始速度自动计算持续时间。 小于1的持续时间(不等于UNDEFINED_DURATION)将导致对scrollBy(int,int)的调用。
boolean withNestedScrolling // 为True时执行嵌套滚动的平滑滚动。 如果持续时间小于0且不等于UNDEFINED_DURATION,则不会发生平滑滚动,因此不会发生嵌套滚动。

我们可以将目标Item距离目标位置的像素计算出来(dx或dy),然后通过这个方法,实现滚动到目标位置。

 

#4. RecyclerView.smoothScrollToPosition(int)

因为ListView有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 (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);
    }
/**
         * <p>Smooth scroll to the specified adapter position.</p>
         * <p>To support smooth scrolling, override this method, create your {@link SmoothScroller}
         * instance and call {@link #startSmoothScroll(SmoothScroller)}.
         * </p>
         * @param recyclerView The RecyclerView to which this layout manager is attached
         * @param state    Current State of RecyclerView
         * @param position Scroll to this adapter position.
         */
        public void smoothScrollToPosition(RecyclerView recyclerView, State state,
                int position) {
            Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling");
        }

Android 它在RecyclerView中并没有给出这个方法的实现,必须重写smoothScrollToPosition以支持平滑滚动,没错,需要自己动手重写实现平滑滚动!!!

如果需要用这个方法,只能自己去重写RecyclerView的smoothScrollToPosition方法,但是,我在紧挨着该源码的下面看到了这个方法:RecyclerView.SmoothScroller

public void startSmoothScroll(SmoothScroller smoothScroller) {
            if (mSmoothScroller != null && smoothScroller != mSmoothScroller
                    && mSmoothScroller.isRunning()) {
                mSmoothScroller.stop();
            }
            mSmoothScroller = smoothScroller;
            mSmoothScroller.start(mRecyclerView, this);
        }

也是这里想要介绍的!

 

### RecyclerView.SmoothScroller


Base class for smooth scrolling. Handles basic tracking of the target view position and provides methods to trigger a programmatic scroll.

An instance of SmoothScroller is only intended to be used once. You should create a new instance for each call to RecyclerView.LayoutManager.startSmoothScroll(SmoothScroller)


大概翻译:用于平滑滚动的基类。 处理目标视图位置的基本跟踪,并提供触发程序化滚动的方法。 SmoothScroller的一个实例只能使用一次。 您应该为每次调用RecyclerView.LayoutManager.startSmoothScroll(SmoothScroller)创建一个新实例。
* 以下代码都是以垂直方向滑动列表做示例:

/**
         * Starts a smooth scroll using the provided {@link SmoothScroller}.
         *
         * <p>Each instance of SmoothScroller is intended to only be used once. Provide a new
         * SmoothScroller instance each time this method is called.
         *
         * <p>Calling this method will cancel any previous smooth scroll request.
         *
         * @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;
            mSmoothScroller.start(mRecyclerView, this);
        }

⚠️ 根据上面的源码就可以看出 SmoothScroller的一个实例只能使用一次!!!

使用起来很简单,两三行代码:

private fun smoothMoveToPosition(position: Int) {
        val smoothScroller = LinearSmoothScroller(context)
        smoothScroller.targetPosition = position
        layoutManager.startSmoothScroll(smoothScroller)
    }

这里多出来一个类 - LinearSmoothScroller,大概意思:

  • 由RecyclerView.SmoothScroller实现,该实现使用LinearInterpolator直到目标位置成为RecyclerView的子级,然后使用DecelerateInterpolator缓慢地接近目标位置。
  • 如果您使用的RecyclerView.LayoutManager没有实现RecyclerView.SmoothScroller.ScrollVectorProvider接口,那么您必须重写computeScrollVectorForPosition(int)方法。与支持库捆绑在一起的所有LayoutManager都实现此接口。

#1. 关于对齐方式的设置:

// 将子视图的左侧或顶部与父视图的左侧或顶部对齐
public static final int SNAP_TO_START = -1;

// 将子视图的右侧或底部与父视图的右侧或底部对齐
public static final int SNAP_TO_END = 1;

// 根据当前相对于父级的位置,决定从子级开始还是结束子级。例如,如果视图实际上位于RecyclerView的左侧,则使用SNAP_TO_ANY与使用SNAP_TO_START相同
public static final int SNAP_TO_ANY = 0;

根据源码中的对齐方式的实现:

/**
     * When scrolling towards a child view, this method defines whether we should align the top
     * or the bottom edge of the child with the parent RecyclerView.
     *
     * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
     * @see #SNAP_TO_START
     * @see #SNAP_TO_END
     * @see #SNAP_TO_ANY
     */
    protected int getVerticalSnapPreference() {
        return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
                mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
    }

所以我们可以尝试去重写该方法: getVerticalSnapPreference()

private fun smoothMoveToPosition(position: Int) {
        val smoothScroller = object : LinearSmoothScroller(context) {
                override fun getVerticalSnapPreference(): Int {
                    return SNAP_TO_START
                }
            }
        smoothScroller.targetPosition = position
        transactionLayoutManager.startSmoothScroll(smoothScroller)
    }

以上的三种对齐方式,可能不足以满足其他的位置,所以想到了能不能设置相对于对齐方式后再偏移一些距离呢?

#2. 设置偏移量:
根据源码,由对齐方式然后再到计算滑动距离的实现:

/**
     * Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
     * {@link #calculateDyToMakeVisible(android.view.View, int)}
     */
    public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
            snapPreference) {
        switch (snapPreference) {
            case SNAP_TO_START:
                return boxStart - viewStart;
            case SNAP_TO_END:
                return boxEnd - viewEnd;
            case SNAP_TO_ANY:
                final int dtStart = boxStart - viewStart;
                if (dtStart > 0) {
                    return dtStart;
                }
                final int dtEnd = boxEnd - viewEnd;
                if (dtEnd < 0) {
                    return dtEnd;
                }
                break;
            default:
                throw new IllegalArgumentException("snap preference should be one of the"
                        + " constants defined in SmoothScroller, starting with SNAP_");
        }
        return 0;
    }

看到如果设置的是SNAP_TO_START,我们可以更改 boxStart - viewStart 的值,所以我们可以尝试去重写该方法: calculateDtToFit()

private fun setSmoothScrollerToRequired(position: Int, offset: Int) =
        object : LinearSmoothScroller(context) {
            override fun getVerticalSnapPreference(): Int {
                return SNAP_TO_START
            }

            override fun calculateDtToFit(
                viewStart: Int,
                viewEnd: Int,
                boxStart: Int,
                boxEnd: Int,
                snapPreference: Int
            ): Int {
                return if (snapPreference == SNAP_TO_START
                    && !countInventoryDecorator.isFirstOfGroup(position)
                ) {
                    boxStart - viewStart + offset
                } else {
                    super.calculateDtToFit(viewStart, viewEnd, boxStart, boxEnd, snapPreference)
                }
            }
        }.apply { targetPosition = position }

再调用方法实现目标Item滚动到指定位置:

layoutManager.startSmoothScroll(setSmoothScrollerToRequired(it, 150))

补充:这里一行代码是我原代码中的条件判断:countInventoryDecorator.isFirstOfGroup(position),可以根据自己的需要去判断哪些item是需要目标Item滚动到你需要的位置。

 

### LinearSmoothScroller还有其他的方式可以重写实现更多的需求,所以这里只是为了 实现目标Item滚动到指定位置所阐述一些理解。

 


补充:重写smoothScrollToPosition以支持平滑滚动,这里再重写的方法里使用RecyclerView.SmoothScroller也很nice!!

@Override
public void smoothScrollToPosition(RecyclerView recyclerView,
                                   RecyclerView.State state,
                                   int position) {
    RecyclerView.SmoothScroller smoothScroller =
            new LinearSmoothScroller(recyclerView.getContext()) {
                @Override
                protected int getVerticalSnapPreference() {
                    return SNAP_TO_START; // override base class behavior
                }
            };
    smoothScroller.setTargetPosition(position);
    startSmoothScroll(smoothScroller);
}