RecyclerView不仅实现在有限窗口显示大数据集,还支持对其中的item视图进行Swipe(轻扫)和Drag(拖拽)操作,这可以借助ItemTouchHelper辅助类轻松实现。
关键代码:
// 1.创建ItemTouchHelper.Callback,实现回调方法 ItemTouchHelper.Callback callback = new ItemTouchHelper.Callback() { // 返回允许滑动的方向 @Override public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { // 返回可滑动方向,通过使用一个int,在各个bit位标记来记录。 // 这里drag支持上下方向,swipe支持左右方向。 int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; // 返回设置了标识位的复合int return makeMovementFlags(dragFlags, swipeFlags); } // 允许drag的前提下,当ItemTouchHelper想要将拖动的项目从其旧位置移动到新位置时调用 @Override public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { // 获取被拖拽item和目标item的适配器索引(适配器索引是该item对应数据集的索引,getLayoutPosition是当前布局的位置) int from = viewHolder.getAdapterPosition(); int to = target.getAdapterPosition(); // 交换数据集的数据 Collections.swap(data, from, to); // 通知Adapter更新 adapter.notifyItemMoved(from, to); // 返回true表示item移到了目标位置 return true; } // 允许swipe的前提下,当用户滑动ViewHolder触发临界时调用 @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { // 获取滑动的item对应的适配器索引 int pos = viewHolder.getAdapterPosition(); // 从数据集移除数据 data.remove(pos); // 通知Adapter更新 adapter.notifyItemRemoved(pos); } }; // 2.传入ItemTouchHelper.Callback ItemTouchHelper touchHelper = new ItemTouchHelper(callback); // 3.将touchHelper和recyclerView绑定 touchHelper.attachToRecyclerView(recyclerView); 复制代码
以上代码三个步骤就可以实现swipe和drag,效果如图:
我们知道RecyclerView作为ViewGroup,有自己的滑动事件处理,那么ItemTouchHelper是如何进行swipe和drag,而不产生冲突。
ItemTouchHelper如何通过attachToRecyclerView方法附加RecyclerView,就能将触摸事件托管到自己身上执行。
ItemTouchHelper.Callback接口中的onMove和onSwiped是在什么时机回调。
我们注意到上面图示中,Drag操作拖拽item到达边界时,RecyclerView会跟着滚动起来,这是如何调度的。
带着这些问题进入源码,看看ItemTouchHelper大致实现机制,就能知道答案。
文中源码基于 'androidx.recyclerview:recyclerview:1.1.0'
首先看attachToRecyclerView方法: [ItemTouchHelper#attachToRecyclerView]
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { if (mRecyclerView == recyclerView) { return; // nothing to do } // 若绑定过其他RecyclerView,则与旧的解除绑定和清理数据 if (mRecyclerView != null) { destroyCallbacks(); } mRecyclerView = recyclerView; if (recyclerView != null) { final Resources resources = recyclerView.getResources(); mSwipeEscapeVelocity = resources .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity); mMaxSwipeVelocity = resources .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity); // 设置回调 setupCallbacks(); } } 复制代码
关键在setupCallbacks方法中: [ItemTouchHelper#setupCallbacks]
private void setupCallbacks() { ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext()); mSlop = vc.getScaledTouchSlop(); // 添加到mRecyclerView的mItemDecorations集合中 mRecyclerView.addItemDecoration(this); // 添加到mRecyclerView的mOnItemTouchListeners集合中 mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); // 添加到mRecyclerView的mOnChildAttachStateListeners集合中 mRecyclerView.addOnChildAttachStateChangeListener(this); // 创建GestureDetector startGestureDetection(); } 复制代码
该方法中进行各类回调监听注册,这些回调监听是实现swipe和drag的关键。
ItemTouchHelper继承ItemDecoration,ItemDecoration作用是装饰item,通常用来绘制分割线。ItemTouchHelper借助其实现item跟随手指移动。
mOnItemTouchListener注册给RecyclerView后,RecyclerView会将事件回调给它,ItemTouchHelper从而能够拦截事件派发自行处理。
ItemTouchHelper实现OnChildAttachStateChangeListener接口,在该接口的onChildViewDetachedFromWindow方法中处理视图detached时进行释放动作或结束动画、清理视图引用。
startGestureDetection方法中会创建GestureDetector,用于监听触摸事件。当触发onLongPress长按时,判断是否开始drag。
接下来看看RecyclerView如何把触摸事件托管给ItemTouchHelper。
[RecyclerView#onInterceptTouchEvent]
public boolean onInterceptTouchEvent(MotionEvent e) { // ··· mInterceptingOnItemTouchListener = null; // 判断是否有OnItemTouchListener拦截事件 if (findInterceptingOnItemTouchListener(e)) { // 若有拦截则取消滚动 cancelScroll(); return true; } // RecyclerView的onInterceptTouchEvent逻辑 ··· } private boolean findInterceptingOnItemTouchListener(MotionEvent e) { int action = e.getAction(); final int listenerCount = mOnItemTouchListeners.size(); // 依次将事件派发给mOnItemTouchListeners保存的listener for (int i = 0; i < listenerCount; i++) { final OnItemTouchListener listener = mOnItemTouchListeners.get(i); if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) { // 若有listener拦截事件并且当前事件不是CANCEL,则用mInterceptingOnItemTouchListener保存该listener,结束遍历 mInterceptingOnItemTouchListener = listener; return true; } } return false; } 复制代码
RecyclerView在onInterceptTouchEvent方法中处理RecyclerView自身事件拦截逻辑前,会先派发给OnItemTouchListener集合,若有OnItemTouchListener处理则RecyclerView自身不再处理。
[RecyclerView#onTouchEvent]
public boolean onTouchEvent(MotionEvent e) { // ··· // 判断是否有OnItemTouchListener消费事件 if (dispatchToOnItemTouchListeners(e)) { // 若有消费事件则取消滚动 cancelScroll(); return true; } // RecyclerView的onTouchEvent逻辑 ··· } private boolean dispatchToOnItemTouchListeners(MotionEvent e) { if (mInterceptingOnItemTouchListener == null) { // 若在onInterceptTouchEvent时没有OnItemTouchListener拦截事件,那么这里 // 还会将事件派发给OnItemTouchListener,但是会过滤掉DOWN事件,避免重复派发。 if (e.getAction() == MotionEvent.ACTION_DOWN) { return false; } return findInterceptingOnItemTouchListener(e); } else { // 若有拦截事件的OnItemTouchListener,则直接交给它的onTouchEvent方法 mInterceptingOnItemTouchListener.onTouchEvent(this, e); final int action = e.getAction(); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // 若是事件序列结束,则清空mInterceptingOnItemTouchListener mInterceptingOnItemTouchListener = null; } return true; } } 复制代码
RecyclerView在onTouchEvent中处理自身的逻辑前,会先将事件派发给OnItemTouchListener,若有消费事件,则RecyclerView自身不再处理。
RecyclerView重写了requestDisallowInterceptTouchEvent方法,在其中也会回调OnItemTouchListener: [RecyclerView#requestDisallowInterceptTouchEvent]
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { final int listenerCount = mOnItemTouchListeners.size(); // 依次调用OnItemTouchListeners集合中listener for (int i = 0; i < listenerCount; i++) { final OnItemTouchListener listener = mOnItemTouchListeners.get(i); // 在ItemTouchHelper的listener中,若传入不希望拦截事件,那么ItemTouchHelper会释放移动 listener.onRequestDisallowInterceptTouchEvent(disallowIntercept); } super.requestDisallowInterceptTouchEvent(disallowIntercept); } 复制代码
RecyclerView在收到触摸事件时,会优先将事件交给OnItemTouchListener,若有事件被消费,则RecyclerView自身不再消费。ItemTouchHelper便是通过OnItemTouchListener来接收事件,触发SWIPE或DRAG。
看看ItemTouchHelper的mOnItemTouchListener实现的对应事件拦截方法。
[OnItemTouchListener#onInterceptTouchEvent]
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) { // GestureDetector监听输入的事件 mGestureDetector.onTouchEvent(event); if (DEBUG) { Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event); } final int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { // 若该event是一个事件序列的开始,则记录触摸点ID和初始坐标位置 mActivePointerId = event.getPointerId(0); mInitialTouchX = event.getX(); mInitialTouchY = event.getY(); obtainVelocityTracker(); // mSelected成员记录当前选中的ViewHolder,默认为null if (mSelected == null) { // 从mRecoverAnimations集合中根据event位置查找对应item的回复动画(回复动画是手指释放时,view自动移动到指定位置的动画) final RecoverAnimation animation = findAnimation(event); if (animation != null) { // animation的mX、mY记录当前view的偏移位置 mInitialTouchX -= animation.mX; mInitialTouchY -= animation.mY; // 结束动画 endRecoverAnimation(animation.mViewHolder, true); // mPendingCleanup缓存detached后待清除状态的view if (mPendingCleanup.remove(animation.mViewHolder.itemView)) { // 恢复view的Elevation属性,将TranslationX、TranslationY重置为0 mCallback.clearView(mRecyclerView, animation.mViewHolder); } // 将该item重新作为选中的ViewHolder select(animation.mViewHolder, animation.mActionState); // 更新mDx、mDy(mDx、mDy记录已经滑动的偏移量) updateDxDy(event, mSelectedFlags, 0); } } } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // 若该event是一个事件序列的结束,则清空触摸点ID和释放选中ViewHolder mActivePointerId = ACTIVE_POINTER_ID_NONE; select(null, ACTION_STATE_IDLE); } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) { // in a non scroll orientation, if distance change is above threshold, we // can select the item // 这个case中的event就是事件序列的中间事件,若触摸点ID存在(前面ACTION_DOWN时会保存) // 获取触摸点索引 final int index = event.findPointerIndex(mActivePointerId); if (DEBUG) { Log.d(TAG, "pointer index " + index); } if (index >= 0) { // 检查是否符合swipe触发条件,若符合则会调用select方法进行选中处理 checkSelectForSwipe(action, event, index); } } if (mVelocityTracker != null) { // 监听event,用于加速度计算 mVelocityTracker.addMovement(event); } // 若有选中的ViewHolder,则返回true表示拦截 return mSelected != null; } 复制代码
该方法主要逻辑就是在ACTION_DOWN时记录初始触摸位置,ACTION_MOVE、ACTION_POINTER_DOWN、ACTION_POINTER_UP时判断是否符合swipe触发条件,ACTION_UP、ACTION_CANCEL时释放。
其中调用checkSelectForSwipe方法检查swipe条件,是触发swipe的关键方法。还有注意到有多个地方会调用select方法,该方法也是关键方法,会处理item选中和释放的操作。
[OnItemTouchListener#onTouchEvent]
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) { // 手势监听 mGestureDetector.onTouchEvent(event); if (DEBUG) { Log.d(TAG, "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event); } // 加速度监听 if (mVelocityTracker != null) { mVelocityTracker.addMovement(event); } if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { return; } final int action = event.getActionMasked(); final int activePointerIndex = event.findPointerIndex(mActivePointerId); if (activePointerIndex >= 0) { // 检查是否触发swipe checkSelectForSwipe(action, event, activePointerIndex); } ViewHolder viewHolder = mSelected; if (viewHolder == null) { // 若不满足swipe条件则返回 return; } // 执行到这里,说明有在swipe或drag的item switch (action) { case MotionEvent.ACTION_MOVE: { // Find the index of the active pointer and fetch its position if (activePointerIndex >= 0) { // 更新滑动偏移量 updateDxDy(event, mSelectedFlags, activePointerIndex); // 如果当前处于drag状态,则会判断是否达到和某个item交换的条件,触发onMove回调 moveIfNecessary(viewHolder); // mScrollRunnable用于处理当用户拖动item超出边缘时触发LayoutManager滚动 mRecyclerView.removeCallbacks(mScrollRunnable); mScrollRunnable.run(); // 触发RecyclerView重绘 mRecyclerView.invalidate(); } break; } case MotionEvent.ACTION_CANCEL: // 若是CANCEL事件(手指划出view范围),则清除加速度计算 if (mVelocityTracker != null) { mVelocityTracker.clear(); } // fall through case MotionEvent.ACTION_UP: // ACTION_CANCEL和ACTION_UP都释放选中 select(null, ACTION_STATE_IDLE); mActivePointerId = ACTIVE_POINTER_ID_NONE; break; case MotionEvent.ACTION_POINTER_UP: { // 多指触摸情况下一个手指抬起,更新触摸点ID和滑动偏移量 final int pointerIndex = event.getActionIndex(); final int pointerId = event.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = event.getPointerId(newPointerIndex); updateDxDy(event, mSelectedFlags, pointerIndex); } break; } } } 复制代码
该方法中也会通过checkSelectForSwipe方法判断是否触发swipe。注意ACTION_MOVE case中是手指拖动滑动的关键代码。
可以看到ItemTouchHelper也会将收到的事件传给GestureDetector和VelocityTracker。其中GestureDetector用于监听长按事件,VelocityTracker用于计算加速度,当手指全部抬起时判断是否swipe移出item。
[ItemTouchHelper]
// 空闲状态,当前没有用户事件或事件尚未触发swipe或drag public static final int ACTION_STATE_IDLE = 0; // 目前正在swipe视图 public static final int ACTION_STATE_SWIPE = 1; // 目前正在drag视图 public static final int ACTION_STATE_DRAG = 2; private int mActionState = ACTION_STATE_IDLE; 复制代码
ItemTouchHelper的mActionState成员用于记录当前状态。
SWIPE和DRAG不能同时触发,接下来分别看下两种操作的触发条件。
ItemTouchHelper接收到开始滑动事件时调用checkSelectForSwipe检查SWIPE: [ItemTouchHelper#checkSelectForSwipe]
void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) { if (mSelected != null || action != MotionEvent.ACTION_MOVE || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) { // mCallback即我们创建的ItemTouchHelper.Callback,可重写isItemViewSwipeEnabled方法禁用Swipe。 // 若已有选中的ViewHolder或当前事件非ACTION_MOVE或当前已处于DRAG中或禁用了Swipe,则返回。 return; } if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { // RecyclerView自身已在滚动中,则返回 return; } // 查找可滑动的ViewHolder final ViewHolder vh = findSwipedView(motionEvent); if (vh == null) { // 没有符合的ViewHolder则返回 return; } // 获取支持滑动的方向,将调用ItemTouchHelper.Callback的getMovementFlags回调方法返回我们设置的方向 final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh); // 取出swipe对应标识位 final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE); // 判断是否有支持swipe的方向 if (swipeFlags == 0) { return; } // mDx and mDy are only set in allowed directions. We use custom x/y here instead of // updateDxDy to avoid swiping if user moves more in the other direction final float x = motionEvent.getX(pointerIndex); final float y = motionEvent.getY(pointerIndex); // Calculate the distance moved // 计算滑动距离 final float dx = x - mInitialTouchX; final float dy = y - mInitialTouchY; // swipe target is chose w/o applying flags so it does not really check if swiping in that // direction is allowed. This why here, we use mDx mDy to check slope value again. // 取绝对值 final float absDx = Math.abs(dx); final float absDy = Math.abs(dy); // 判断距离是否达到最小滑动距离 if (absDx < mSlop && absDy < mSlop) { return; } // 判断滑动方向,水平和垂直滑动偏移,哪个方向大,就属于哪个方向 if (absDx > absDy) { // 手指向左划,判断是否支持左滑 if (dx < 0 && (swipeFlags & LEFT) == 0) { return; } // 判断是否支持右划 if (dx > 0 && (swipeFlags & RIGHT) == 0) { return; } } else { // 手指向上划,判断是否支持上滑 if (dy < 0 && (swipeFlags & UP) == 0) { return; } // 判断是否支持下滑 if (dy > 0 && (swipeFlags & DOWN) == 0) { return; } } // 执行到这里说明找到ViewHolder且满足SWIPE条件。 // 新开始SWIPE,重置变量 mDx = mDy = 0f; mActivePointerId = motionEvent.getPointerId(0); // 传入ViewHolder和SWIPE对应状态 select(vh, ACTION_STATE_SWIPE); } 复制代码
该方法中提供findSwipedView方法查找一个ViewHolder进行SWIPE,看看这个方法: [ItemTouchHelper#findSwipedView]
private ViewHolder findSwipedView(MotionEvent motionEvent) { // 获取LayoutManager final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { return null; } final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); // 计算滑动偏移量 final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX; final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY; final float absDx = Math.abs(dx); final float absDy = Math.abs(dy); // 判断滑动偏移量是否达到最小滑动距离 if (absDx < mSlop && absDy < mSlop) { return null; } if (absDx > absDy && lm.canScrollHorizontally()) { // 若偏向水平滑动且当前LayoutManager也可水平滑动,为避免冲突,则不能SWIPE // (例如设置了水平排布的LinearLayoutManager,则不能进行水平方向的SWIPE操作) return null; } else if (absDy > absDx && lm.canScrollVertically()) { // 同上,若垂直滑动且LayoutManager也可垂直滑动,不能SWIPE return null; } // 根据event位置查找view View child = findChildView(motionEvent); if (child == null) { return null; } // 返回view对应的ViewHolder return mRecyclerView.getChildViewHolder(child); } 复制代码
接着看findChildView方法: [ItemTouchHelper#findChildView]
View findChildView(MotionEvent event) { // first check elevated views, if none, then call RV final float x = event.getX(); final float y = event.getY(); if (mSelected != null) { final View selectedView = mSelected.itemView; // 若存在选中的ViewHolder,则判断触摸点位置是否落于该view范围中 if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) { return selectedView; } } for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { // 若存在回复动画,依次判断触摸点位置是否落于动画执行的view范围中 final RecoverAnimation anim = mRecoverAnimations.get(i); final View view = anim.mViewHolder.itemView; if (hitTest(view, x, y, anim.mX, anim.mY)) { return view; } } // 从上往下遍历RecyclerView的子view,获取触摸点位置落于的view return mRecyclerView.findChildViewUnder(x, y); } 复制代码
简单总结触发SWIPE的条件:首先计算滑动距离和滑动方向,需要满足最小滑动距离且不能和LayoutManager的滑动方向冲突,根据触摸点位置获取对应的view的ViewHolder。接着判断我们通过ItemTouchHelper.Callback设置的标识位,是否允许swipe和当前方向swipe。若都满足,则调用select方法,传入ViewHolder和ACTION_STATE_SWIPE,进行选中判断操作。
DRAG是在长按时才会触发,ItemTouchHelper通过GestureDetector监听MotionEvent,长按时触发onLongPress回调: [ItemTouchHelperGestureListener#onLongPress]
public void onLongPress(MotionEvent e) { if (!mShouldReactToLongPress) { return; } // 根据触摸点位置获取对应的view View child = findChildView(e); if (child != null) { // 获取view对应的ViewHolder ViewHolder vh = mRecyclerView.getChildViewHolder(child); if (vh != null) { // 判断是否支持drag,将触发ItemTouchHelper.Callback的getMovementFlags, // 若有设置drag方向则会返回true if (!mCallback.hasDragFlag(mRecyclerView, vh)) { return; } int pointerId = e.getPointerId(0); // Long press is deferred. // Check w/ active pointer id to avoid selecting after motion // event is canceled. if (pointerId == mActivePointerId) { final int index = e.findPointerIndex(mActivePointerId); final float x = e.getX(index); final float y = e.getY(index); // 保存初始触摸位置坐标 mInitialTouchX = x; mInitialTouchY = y; mDx = mDy = 0f; if (DEBUG) { Log.d(TAG, "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY); } // 判断是否允许drag,默认返回true if (mCallback.isLongPressDragEnabled()) { // 进行选中操作 select(vh, ACTION_STATE_DRAG); } } } } } 复制代码
可以看到在长按回调中,判断若支持drag,则也调用select方法,传入长按的ViewHolder和ACTION_STATE_DRAG。
在前文中看到,当触发SWIPE或DRAG时,和ACTION_CANCEL、ACTION_UP时,均会调用select。
时机 | 参数selected | 参数actionState |
---|---|---|
触发SWIPE | 按住的ViewHolder | ACTION_STATE_SWIPE |
触发DRAG | 按住的ViewHolder | ACTION_STATE_DRAG |
抬起释放 | null | ACTION_STATE_IDLE |
select方法中包含选中和释放的逻辑,先看选中部分: [ItemTouchHelper#select]
void select(@Nullable ViewHolder selected, int actionState) { if (selected == mSelected && actionState == mActionState) { // 避免重复调用 return; } mDragScrollStartTimeInMs = Long.MIN_VALUE; final int prevActionState = mActionState; // prevent duplicate animations endRecoverAnimation(selected, true); mActionState = actionState; if (actionState == ACTION_STATE_DRAG) { // 若是触发DRAG if (selected == null) { throw new IllegalArgumentException("Must pass a ViewHolder when dragging"); } // we remove after animation is complete. this means we only elevate the last drag // child but that should perform good enough as it is very hard to start dragging a // new child before the previous one settles. // 保存选中的view的引用 mOverdrawChild = selected.itemView; // 如果是小于21的版本,则会设置ViewGroup采用自定义遍历child规则 addChildDrawingOrderCallback(); } int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState)) - 1; boolean preventLayout = false; // 如果之前有选中的ViewHolder,则要对其释放 if (mSelected != null) { // 省略释放的逻辑 ··· } // 如果当前是释放,则selected为null,否则为将选中的ViewHolder if (selected != null) { mSelectedFlags = (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask) >> (mActionState * DIRECTION_FLAG_COUNT); // 记录将选中view的左上角 mSelectedStartX = selected.itemView.getLeft(); mSelectedStartY = selected.itemView.getTop(); // mSelected赋值为将选中的ViewHolder mSelected = selected; if (actionState == ACTION_STATE_DRAG) { // 若是触发DRAG,则给用户一个触觉反馈 mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } } final ViewParent rvParent = mRecyclerView.getParent(); if (rvParent != null) { // 选中时请求父布局不拦截事件,释放时反之 rvParent.requestDisallowInterceptTouchEvent(mSelected != null); } if (!preventLayout) { // 使RecyclerView在下一次布局时运行SimpleAnimation mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout(); } // 回调给ItemTouchHelper.Callback的onSelectedChanged mCallback.onSelectedChanged(mSelected, mActionState); // 触发RecyclerView重绘 mRecyclerView.invalidate(); } 复制代码
可以看出当触发swipe或drag时,主要逻辑是保存选中view的左上角坐标和ViewHolder引用。
如果是drag的话,则还会额外保存选中的view的引用和设置ViewGroup遍历child的自定义顺序(API<21) ,这样做的目的是为了在拖动view时,使这个view保持在其他view上面。我们知道ViewGroup在绘制child时,默认是按照mChildren数组的顺序遍历,为了使指定的child位于上层,在API<21可以通过设置自定义遍历规则,让指定view在最后绘制。在API>=21可以通过设置elevation使之位于上层。
接着看释放时的逻辑: [ItemTouchHelper#select]
void select(@Nullable ViewHolder selected, int actionState) { if (selected == mSelected && actionState == mActionState) { return; } mDragScrollStartTimeInMs = Long.MIN_VALUE; final int prevActionState = mActionState; // prevent duplicate animations endRecoverAnimation(selected, true); mActionState = actionState; // 省略ACTION_STATE_DRAG部分 ··· int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState)) - 1; boolean preventLayout = false; // 如果之前有选中的ViewHolder,则要对其释放 if (mSelected != null) { final ViewHolder prevSelected = mSelected; // 判断之前选中view是否还依附于父布局 if (prevSelected.itemView.getParent() != null) { // 若之前不是drag操作,则获取滑动方向。 // 调用Callback.getMovementFlags获取我们设置的方向,然后会判断滑动加速度 // 是否超过Callback.getSwipeEscapeVelocity设置的阈值或滑动距离是否超出 // Callback.getSwipeThreshold设置的阈值。若超过,则view将被滑走,swipeDir是滑走的那个方向。 // 否则swipeDir是0。 final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 : swipeIfNecessary(prevSelected); releaseVelocityTracker(); // find where we should animate to final float targetTranslateX, targetTranslateY; int animationType; // 根据滑动方向计算偏移距离 switch (swipeDir) { case LEFT: case RIGHT: case START: case END: // 水平方向,Y轴不变,X轴上view要移动到RecyclerView边界外 targetTranslateY = 0; targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth(); break; case UP: case DOWN: targetTranslateX = 0; targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight(); break; default: targetTranslateX = 0; targetTranslateY = 0; } // 记录动画类型 if (prevActionState == ACTION_STATE_DRAG) { animationType = ANIMATION_TYPE_DRAG; } else if (swipeDir > 0) { // ItemTouchHelper将完全滑走视为SWIPE成功 animationType = ANIMATION_TYPE_SWIPE_SUCCESS; } else { // 回到原始位置视为SWIPE取消 animationType = ANIMATION_TYPE_SWIPE_CANCEL; } // 计算选中view当前的X、Y轴偏移量,保存在mTmpPosition数组中 getSelectedDxDy(mTmpPosition); final float currentTranslateX = mTmpPosition[0]; final float currentTranslateY = mTmpPosition[1]; // 创建RecoverAnimation,内部封装属性动画操作。将view从currentTranslate移动到targetTranslate。 final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType, prevActionState, currentTranslateX, currentTranslateY, targetTranslateX, targetTranslateY) { @Override public void onAnimationEnd(Animator animation) { // 动画完成 super.onAnimationEnd(animation); // 若动画期间,用户又触摸该view,mOverridden会标记为true if (this.mOverridden) { return; } if (swipeDir <= 0) { // this is a drag or failed swipe. recover immediately mCallback.clearView(mRecyclerView, prevSelected); // full cleanup will happen on onDrawOver } else { // 滑出动画结束 // wait until remove animation is complete. // 将view保存进mPendingCleanup集合待后续清除 mPendingCleanup.add(prevSelected.itemView); mIsPendingCleanup = true; if (swipeDir > 0) { // 针对swipe滑走view情况 // Animation might be ended by other animators during a layout. // We defer callback to avoid editing adapter during a layout. // 发送主线程,待没有动画执行时,回调Callback.onSwiped。 // 在该回调中,我们将对应的item从适配器数据集中移除。 postDispatchSwipe(this, swipeDir); } } // removed from the list after it is drawn for the last time if (mOverdrawChild == prevSelected.itemView) { // 针对drag情况,触发drag时mOverdrawChild赋值为选中view, // 这里需要清理引用和取消ViewGroup自定义遍历child规则。 removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); } } }; final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); rv.setDuration(duration); // 将动画保存进mRecoverAnimations集合 mRecoverAnimations.add(rv); // 启动动画 rv.start(); // preventLayout标记为true,RecyclerView将不执行SimpleAnimation preventLayout = true; } else { removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); mCallback.clearView(mRecyclerView, prevSelected); } // mSelected置为null mSelected = null; } if (selected != null) { // 省略选中时的逻辑 ··· } final ViewParent rvParent = mRecyclerView.getParent(); if (rvParent != null) { // 选中时请求父布局不拦截事件,释放时反之 rvParent.requestDisallowInterceptTouchEvent(mSelected != null); } if (!preventLayout) { // 使RecyclerView在下一次布局时运行SimpleAnimation mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout(); } // 回调给ItemTouchHelper.Callback的onSelectedChanged mCallback.onSelectedChanged(mSelected, mActionState); // 触发RecyclerView重绘 mRecyclerView.invalidate(); } 复制代码
可以看出释放时,会判断swipe还是drag和是否将view滑走,计算translate创建RecoverAnimation执行属性动画。动画完成后,若是swipe滑走,则发送到主线程待没有任何动画时回调Callback.onSwiped。若是drag,则将之前设置的ViewGroup自定义遍历child规则取消。
当select选中ViewHolder和onTouchEvent处理ACTION_MOVE时都会触发RecyclerView.invalidate重绘。
RecyclerView重写了draw和onDraw方法,看看这两个方法: [RecyclerView#onDraw、RecyclerView#draw]
public void onDraw(Canvas c) { super.onDraw(c); final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDraw(c, this, mState); } } public void draw(Canvas c) { super.draw(c); final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDrawOver(c, this, mState); } // ··· } 复制代码
ItemTouchHelper继承ItemDecoration,在和RecyclerView绑定时添加进mItemDecorations集合,因此当重绘时,会先后回调ItemTouchHelper的onDraw和onDrawOver方法。
先进入onDraw方法: [ItemTouchHelper#onDraw]
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { // we don't know if RV changed something so we should invalidate this index. mOverdrawChildPosition = -1; float dx = 0, dy = 0; if (mSelected != null) { // 计算偏移量 getSelectedDxDy(mTmpPosition); dx = mTmpPosition[0]; dy = mTmpPosition[1]; } // 回调Callback的onDraw方法 mCallback.onDraw(c, parent, mSelected, mRecoverAnimations, mActionState, dx, dy); } 复制代码
[Callback#onDraw]
void onDraw(Canvas c, RecyclerView parent, ViewHolder selected, List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, int actionState, float dX, float dY) { final int recoverAnimSize = recoverAnimationList.size(); for (int i = 0; i < recoverAnimSize; i++) { // 若存在回复动画,则更新动画中的view的偏移量 final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); anim.update(); final int count = c.save(); onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, false); c.restoreToCount(count); } if (selected != null) { // 保存画布当前状态 final int count = c.save(); // 将调用ItemTouchUIUtilImpl的onDraw方法 onChildDraw(c, parent, selected, dX, dY, actionState, true); // 恢复画布到原来状态 c.restoreToCount(count); } } 复制代码
关键逻辑在ItemTouchUIUtilImpl的onDraw方法中: [ItemTouchUIUtilImpl#onDraw]
public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY, int actionState, boolean isCurrentlyActive) { // 当API>=21时,计算最大的elevation值设置给view,使它位于最上层 if (Build.VERSION.SDK_INT >= 21) { if (isCurrentlyActive) { Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation); if (originalElevation == null) { originalElevation = ViewCompat.getElevation(view); float newElevation = 1f + findMaxElevation(recyclerView, view); ViewCompat.setElevation(view, newElevation); view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation); } } } // 更新view的偏移量 view.setTranslationX(dX); view.setTranslationY(dY); } 复制代码
可以看出swipe和drag的滑动和拖动,是通过ItemTouchHelper监听RecyclerView重绘,不断更新view的位移坐标来实现的。
onDrawOver方法中的逻辑和onDraw类似,区别是多了对mRecoverAnimations的清理判断工作,会回调ItemTouchUIUtilImpl的onDrawOver方法,但是该方法是空实现。
前文分析过OnItemTouchListener.onTouchEvent中在ACTION_MOVE会判断是否触发RecyclerView滚动: [OnItemTouchListener#onTouchEvent]
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) { mGestureDetector.onTouchEvent(event); // ··· switch (action) { case MotionEvent.ACTION_MOVE: { // Find the index of the active pointer and fetch its position if (activePointerIndex >= 0) { // 更新滑动偏移量 updateDxDy(event, mSelectedFlags, activePointerIndex); // 如果当前处于drag状态,则会判断是否达到和某个item交换的条件,触发onMove回调 moveIfNecessary(viewHolder); // mScrollRunnable用于处理当用户拖动item超出边缘时触发LayoutManager滚动 mRecyclerView.removeCallbacks(mScrollRunnable); mScrollRunnable.run(); // 触发RecyclerView重绘 mRecyclerView.invalidate(); } break; } // ··· } // ··· } 复制代码
进入moveIfNecessary方法: [ItemTouchHelper#moveIfNecessary]
void moveIfNecessary(ViewHolder viewHolder) { if (mRecyclerView.isLayoutRequested()) { return; } if (mActionState != ACTION_STATE_DRAG) { return; } final float threshold = mCallback.getMoveThreshold(viewHolder); final int x = (int) (mSelectedStartX + mDx); final int y = (int) (mSelectedStartY + mDy); // 判断拖动距离的阈值是否达到view宽或高的一半 if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold && Math.abs(x - viewHolder.itemView.getLeft()) < viewHolder.itemView.getWidth() * threshold) { return; } // 查找所有和选中view有交叉重叠的其他child,并预计算之间的距离 List<ViewHolder> swapTargets = findSwapTargets(viewHolder); if (swapTargets.size() == 0) { return; } // may swap. // 找到一个可交换的ViewHolder。(以垂直拖动为例,若往上拖拽,则比较选中view的上边界 // 是否小于目标view的上边界,往下拖拽则以下选中view和目标view的下边界作为临界值。 // 如果有多个view满足,以差值最大的作为目标view) ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y); if (target == null) { // 若没有找到可交换的ViewHolder,则清空集合返回 mSwapTargets.clear(); mDistances.clear(); return; } final int toPosition = target.getAdapterPosition(); final int fromPosition = viewHolder.getAdapterPosition(); // 回调Callback.onMove,我们在此方法中进行适配器数据集中的item交换 if (mCallback.onMove(mRecyclerView, viewHolder, target)) { // 若进行了数据交换,onMove需要返回true。 // onMoved的默认实现中会判断目标view的边界是否超出RecyclerView和LayoutManager是否支持对应方向滚动, // 进而调用RecyclerView.scrollToPosition方法,滚动到指定索引位置。 // keep target visible mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, target, toPosition, x, y); } } 复制代码
可见moveIfNecessary方法是drag拖动的关键,在拖动过程中判断选中view和其他view的边界作为临界值,作为触发onMove的条件。并且在item交换成功后,还会判断目标view是否超出RecyclerView,继而触发滚动。
回到onTouchEvent方法的ACTION_MOVE case中,在执行完moveIfNecessary后,接着执行mScrollRunnable.run(),看看这个方法:
final Runnable mScrollRunnable = new Runnable() { @Override public void run() { // scrollIfNecessary若有滚动则返回true if (mSelected != null && scrollIfNecessary()) { if (mSelected != null) { //it might be lost during scrolling moveIfNecessary(mSelected); } mRecyclerView.removeCallbacks(mScrollRunnable); ViewCompat.postOnAnimation(mRecyclerView, this); } } }; 复制代码
scrollIfNecessary方法中判断选中view的边界是否超出RecyclerView和LayoutManager是否支持对应方向滚动,若满足则计算滚动偏移量,并通过Callback.interpolateOutOfBoundsScroll计算差值偏移,最后调用RecyclerView.scrollBy触发滚动。
mScrollRunnable和moveIfNecessary中都有可能触发滚动。区别是mScrollRunnable中是当选中view拖拽超出边界时,通过RecyclerView.scrollBy方法滚动一定偏移距离。moveIfNecessary中时当交换item后,判断目标view超出边界,通过RecyclerView.scrollToPosition方法滚动到目标view指定索引位置。
通过对swipe和drag的过程的源码分析,将ItemTouchHelper拆解为初始注册绑定、事件托管、事件拦截处理、SWIPE和DRAG触发判定、选中View的拖动和释放处理、DRAG交换和超出边界滚动等部分,对ItemTouchHelper的实现机制有了大概的了解。