RecyclerView 滑动控制笔记

为了将指定的 item 平滑的滑动至置顶。

  • 简单的直接置顶:
mLayoutManager.scrollToPositionWithOffset(position, 0);//没有滑动,直接置顶

缺点是体验不好,没有滑动的过程。

  • RecyclerView 的滑动方法
rv.scrollToPosition(index);//直接定位,没有滑动的过程,不能置顶
rv.smoothScrollToPosition(position);//平滑的滑动,不能置顶
rv.scrollBy(int x, int y);

scrollToPosition();smoothScrollToPosition() 将对应的 item 滑动到屏幕内,当 item 变为可见时则停止滑动。
所以当指定的 item 在当前屏幕的下方时,滑动后目标 item 会出现屏幕的最底部;
当指定 item 在屏幕可见时,则完全没有滑动。
很多时候这个方法是不符合我们预期的,有时候,我们希望能将指定的 item 平滑的滑动到当前的屏幕顶端或中间。
这时候可以配合 scrollBy() 来做一个判断:

private void setSelectPosition(int index) {
    //当前可见的第一项和最后一项
    int firstItem = linearLayoutManager.findFirstVisibleItemPosition();
    int lastItem = linearLayoutManager.findLastVisibleItemPosition();
    if (index <= firstItem) {
        //当要置顶的项在当前显示的第一个项的前面时,直接调用没有问题
        rv.scrollToPosition(index);
    } else if (index <= lastItem) {
        //当要置顶的项已经在屏幕上显示时,计算需要滑动的距离
        int top = rv.getChildAt(index - firstItem).getTop();
        rv.scrollBy(0, top);
    } else {
        //当指定的 item 在当前显示的最后一项的后面时
        //这时候一次的滑动不足以将指定 item 放到顶端
        rv.scrollToPosition(index);
        //记录当前需要在RecyclerView滚动监听里面继续第二次滚动
        move = true;
    }
}
//在第三种的情况中,scrollToPosition() 结束后该方法被调用
class RecyclerViewListener extends RecyclerView.OnScrollListener{
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            //在这里进行第二次滚动
            if (move ){
                move = false;
                int n = mIndex - mLinearLayoutManager.findFirstVisibleItemPosition();
                if ( 0 <= n && n < mRecyclerView.getChildCount()){
                    //要移动的距离
                    int top = mRecyclerView.getChildAt(n).getTop();
                    mRecyclerView.scrollBy(0, top);
                }
            }
        }
    }

上面这个方面也是看到别人的实现的思路,虽然没什么问题不过还是算是取巧的一种方法,仅记录。

其实 Stack Overflow 上已经有了更好的答案 :RecyclerView – How to smooth scroll to top of item on a certain position?

  • 重写 LinearLayoutManager
public class LinearLayoutManagerWithSmoothScroller extends LinearLayoutManager {

    public LinearLayoutManagerWithSmoothScroller(Context context) {
        super(context, VERTICAL, false);
    }

    public LinearLayoutManagerWithSmoothScroller(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
    }

    //重点方法
    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
                                       int position) {

        //也就是说重点在于重写 SmoothScroller,而滑动的调用为 startSmoothScroll()
        RecyclerView.SmoothScroller smoothScroller = new  TopSnappedSmoothScroller(recyclerView.getContext());
        smoothScroller.setTargetPosition(position);
        startSmoothScroll(smoothScroller);
    }

    private class TopSnappedSmoothScroller extends LinearSmoothScroller {
        public TopSnappedSmoothScroller(Context context) {
            super(context);
        }

        @Override
        public PointF computeScrollVectorForPosition(int targetPosition) {
            return LinearLayoutManagerWithSmoothScroller.this
                    .computeScrollVectorForPosition(targetPosition);
        }

        @Override
        protected int getVerticalSnapPreference() {
            //将指定的 item 滑动至与屏幕的顶端对齐
            return SNAP_TO_START;
        }
    }
}

//使用
recyclerView.smoothScrollToPosition(position);

仔细看可以发现我们也可以选择不重写整个 LinearLayoutManager,只要将 LinearSmoothScroller 的 getVerticalSnapPreference() 重写也可以达到目的。

关于 getVerticalSnapPreference () 的源码与注释:

    /**
     * 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;
    }

返回的值决定了指定 item 的对齐方式,与顶部对齐 / 底部对齐。
所以最终代码:

//初始化过程
mLayoutManager= new LinearLayoutManager(getActivity());
mSmoothScroller = new LinearSmoothScroller(getActivity()) {
    @Override protected int getVerticalSnapPreference() {
        return LinearSmoothScroller.SNAP_TO_START;
     }

    @Override
    public PointF computeScrollVectorForPosition(int targetPosition) {
        return mLayoutManager.computeScrollVectorForPosition(targetPosition);
    }
};
mRvRetail.setLayoutManager(mLayoutManager);

//滑动
mSmoothScroller.setTargetPosition(position);
mLayoutManager.startSmoothScroll(mSmoothScroller);
点赞