当RecyclerView拖拽排序遇见CoordinatorLayout+AppBarLayout

问题

在实现RecyclerView排序的功能时,当配合CoordinatorLayout+AppBarLayout+app:layout_behavior="@string/appbar_scrolling_view_behavior"会出现无法拖动排序的现象:

item_touch_helper.gif

本文就此现象阐述其发生的原因与解决方法。

知识前提

嵌套滑动机制
itemTouchHelper原理
CoordinatorLayout+AppBarLayout原理

拖动排序主要由Google已经封装好的ItemTouchHelper实现;
RecyclerView的滑动联动效果涉及到嵌套滑动与CoordinatorLayout机制;
需要了解相应的原理知识。网上已经有许多优秀的讲解文章,故不在此班门弄斧。

简而言之

  • CoordinatorLayout通过behavior机制使直接子View间获得互相通知状态(位置、嵌套滑动...)的机会,子View可以在其中做出对应改变;
  • RecyclerView通过设置app:layout_behavior="@string/appbar_scrolling_view_behavior"(即AppBarLayout$ScrollingViewBehavior),根据AppBarLayout高度设置自身偏移:

image

  • AppBarLayout通过AppBarLayout.Behavior监听嵌套滑动实现联动效果;
  • 拖动排序简要流程:

选中item,开始拖动 -> item是否拖动超过RecyclerView边界,判断是否滚动RecyclerView -> item边界检查,判断是否交换item位置 -> RecyclerView边界检查 -> item边界检查 ..... -> 松开Item,结束拖动

原因

可以观察出无法拖动排序时RecyclerView因为AppBarLayout的高度影响有较大的偏移量,由 ItemTouchHelper 的原理易知,原因为item拖动时无法移动到RecyclerView的边界处进行滚动,进而无法继续排序。

解决方法

  • 只要使拖拽排序时的item正常接近RecyclerView边界即可,即在拖拽过程中纠正RecyclerView的偏移量;
  • RecyclerView的偏移量由AppBarLayout决定(最大偏移量为AppBarLayout高度,实时偏移量由RecyclerView滚动产生的嵌套滑动决定);
  • 从 ItemTouchHelper 源码看,item在拖拽时进入RecyclerView mOnItemTouchListeneronTouchEvent方法中,已经脱离了嵌套滑动机制的范畴;

所以我们可以在 ItemTouchHelper 的边界检查中通过触发嵌套滑动使得 AppBarLayout 产生联动,从而纠正偏移量。 翻看ItemTouchHelper源码,发现处理边界检查方法scrollIfNecessary为私有方法,无法继承重写,从逻辑上看也没有可供取巧的点位,那我们只能copy一份源码,在关键位置加入相对应的处理。 解决方案有了,接下来有两个问题:

  • 怎样获取RecyclerView的偏移量
  • 如何触发嵌套滑动

对于偏移量:简单一点可以直接取RecyclerView的可见高度与实际高度,其差值则为偏移量。
触发嵌套滑动:可以根据机制构造一个嵌套滑动的流程来实现。

则除原代码不变,添加逻辑如下:

    boolean scrollIfNecessary() {
        ......
        //只处理上下拖动
        if (lm.canScrollVertically()) {
            int curY = (int) (mSelectedStartY + mDy);
            final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop();
            //向上拖动,原逻辑
            if (mDy < 0 && topDiff < 0) {
                scrollY = topDiff;
            } else if (mDy > 0) { //向下拖动

                //可见范围
                mRecyclerView.getGlobalVisibleRect(mRecyclerviewGlobalVisibleRect, null);
                //mRecyclerView底部坐标
                int recyclerViewBottom = mRecyclerviewGlobalVisibleRect.top + mRecyclerView.getHeight();
                //被AppBarLayout$ScrollingViewBehavior偏移的距离
                int offsetOfRecyclerView = recyclerViewBottom - mRecyclerviewGlobalVisibleRect.bottom;

                final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom
                        - (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom())
                        //加上偏移量
                        + offsetOfRecyclerView;

                if (bottomDiff > 0) {

                    //如果偏移量大于0,先消耗偏移量
                    if (offsetOfRecyclerView > 0) {
                        //需要滚动的量
                        int needScrollDistance = this.mCallback.interpolateOutOfBoundsScroll(this.mRecyclerView, this.mSelected.itemView.getHeight(), bottomDiff, this.mRecyclerView.getHeight(), scrollDuration);
                        //嵌套滑动消耗
                        int nestedScrollConsume = Math.min(needScrollDistance, offsetOfRecyclerView);

                        //构造嵌套滑动
                        mRecyclerView.startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                        mRecyclerView.dispatchNestedPreScroll(0, nestedScrollConsume, null, null);
                        mRecyclerView.dispatchNestedScroll(0, 0, 0, nestedScrollConsume, null);
                        mRecyclerView.stopNestedScroll();

                        if (mDragScrollStartTimeInMs == Long.MIN_VALUE) {
                            mDragScrollStartTimeInMs = now;
                        }
                        //修正mDy
                        mDy += nestedScrollConsume;
                        //消除抖动
                        mRecyclerView.invalidate();

                        if (nestedScrollConsume == needScrollDistance) {
                            //偏移量与滑动值相等时,scrollY == 0,不会开始滚动,这里直接返回true,保持开始滑动的逻辑
                            return true;
                        } else {
                            //剩余偏移量
                            scrollY = needScrollDistance - offsetOfRecyclerView;
                        }
                    } else {
                        //原逻辑
                        scrollY = bottomDiff;
                    }
                }
            }
        }
        ......
    }
复制代码

效果:

nestscroll_touch_helper.gif

Code:NestScrollableItemTouchHelper

猜你喜欢

转载自juejin.im/post/7030808130866905101
今日推荐