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