问题
在实现RecyclerView排序的功能时,当配合CoordinatorLayout
+AppBarLayout
+app:layout_behavior="@string/appbar_scrolling_view_behavior"
会出现无法拖动排序的现象:
本文就此现象阐述其发生的原因与解决方法。
知识前提
嵌套滑动机制
itemTouchHelper原理
CoordinatorLayout+AppBarLayout原理
拖动排序主要由Google已经封装好的ItemTouchHelper实现;
RecyclerView的滑动联动效果涉及到嵌套滑动与CoordinatorLayout机制;
需要了解相应的原理知识。网上已经有许多优秀的讲解文章,故不在此班门弄斧。
简而言之
- CoordinatorLayout通过behavior机制使直接子View间获得互相通知状态(位置、嵌套滑动...)的机会,子View可以在其中做出对应改变;
- RecyclerView通过设置
app:layout_behavior="@string/appbar_scrolling_view_behavior"
(即AppBarLayout$ScrollingViewBehavior),根据AppBarLayout高度设置自身偏移:
- 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
mOnItemTouchListener
的onTouchEvent
方法中,已经脱离了嵌套滑动机制的范畴;
所以我们可以在 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;
}
}
}
}
......
}
复制代码
效果: