欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

RecyclerView - 实现目标Item滚动到指定位置(SmoothScroller)

程序员文章站 2022-03-27 08:55:17
scrollToPositionWithOffset:offset - 项目视图的起始边缘与RecyclerView的起始边缘之间的距离(以像素为单位)。这里相比scrollToPosition,我们就可以设置偏移量:如果offset = 0,我们可以理解为将目标Item刻意的滚动到顶部第一个可见位置,如果offset = 100,将目标Item刻意的滚动到距离顶部第一个可见位置往下偏移100px,然后以此类推...如果只是想使某个位置可见,请使用scrollToPosition(int)...

如果需要实现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))

### 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);
}

 

本文地址:https://blog.csdn.net/qq_20613731/article/details/112854243