ItemTouchHelper是一个强大的帮助类。用来配合RecyclerView使用,ItemTouchHelper同一时刻只能支持两种效果:swipe、drag中的一种。分别用来实现RecyclerView里面item侧滑删除(swipe)效果或者item长按拖拽移动(drag)。当然swipe和drag效果同一时刻只能支持一种。因为事件冲突不能同时支持。
实例代码下载地址
ItemTouchHelper使用过程中最关键的点其实就是一个回调类的使用,我们上层所有的逻辑操作都在这个类的回调函数中实现。而且这个类必须继承自ItemTouchHelper.Callback类。这里我帮大家总结出来了ItemTouchHelper.Callback里面常用函数。同时相关的解释如下:
/**
* 针对swipe和drag状态,设置不同状态(swipe、drag)下支持的方向
* (LEFT, RIGHT, START, END, UP, DOWN)
* idle:0~7位表示swipe和drag的方向
* swipe:8~15位表示滑动方向
* drag:16~23位表示拖动方向
*/
public abstract int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder);
/**
* 针对swipe和drag状态,当swipe或者drag对应的ViewHolder改变的时候调用
* 我们可以通过重写这个函数获取到swipe、drag开始和结束时机,viewHolder 不为空的时候是开始,空的时候是结束
*/
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
}
/**
* 针对swipe状态,是否允许swipe(滑动)操作
*/
public boolean isItemViewSwipeEnabled() {
return true;
}
/**
* 针对swipe状态,swipe滑动的位置超过了百分之多少就消失
*/
public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
return .5f;
}
/**
* 针对swipe状态,swipe的逃逸速度,换句话说就算没达到getSwipeThreshold设置的距离,达到了这个逃逸速度item也会被swipe消失掉
*/
public float getSwipeEscapeVelocity(float defaultValue) {
return defaultValue;
}
/**
* 针对swipe状态,swipe滑动的阻尼系数,设置最大滑动速度
*/
public float getSwipeVelocityThreshold(float defaultValue) {
return defaultValue;
}
/**
* 针对swipe状态,swipe 到达滑动消失的距离回调函数,一般在这个函数里面处理删除item的逻辑
* 确切的来讲是swipe item滑出屏幕动画结束的时候调用
*/
public abstract void onSwiped(RecyclerView.ViewHolder viewHolder, int direction);
/**
* 针对drag状态,当item长按的时候是否允许进入drag(拖动)状态
*/
public boolean isLongPressDragEnabled() {
return true;
}
/**
* 针对drag状态,当前target对应的item是否允许move
* 换句话说我们一般用drag来做一些换位置的操作,就是当前target对应的item是否可以换位置
*/
public boolean canDropOver(RecyclerView recyclerView, RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) {
return true;
}
/**
* 针对drag状态,在canDropOver()函数返回true的情况下,会调用该函数让我们去处理拖动换位置的逻辑(需要重写自己处理变换位置的逻辑)
* 如果有位置变换返回true,否则发挥false
*/
public abstract boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target);
/**
* 针对drag状态,当drag itemView和底下的itemView重叠的时候,可以给drag itemView设置额外的margin,让重叠更加容易发生。
* 相当于增大了drag itemView的区域
*/
public int getBoundingBoxMargin() {
return 0;
}
/**
* 针对drag状态,滑动超过百分之多少的距离可以可以调用onMove()函数(注意哦,这里指的是onMove()函数的调用,并不是随手指移动的那个view哦)
*/
public float getMoveThreshold(RecyclerView.ViewHolder viewHolder) {
return .5f;
}
/**
* 针对drag状态,在drag的过程中获取drag itemView底下对应的ViewHolder(一般不用我们处理直接super就好了)
*/
public RecyclerView.ViewHolder chooseDropTarget(RecyclerView.ViewHolder selected,
List dropTargets,
int curX,
int curY) {
return super.chooseDropTarget(selected, dropTargets, curX, curY);
}
/**
* 当onMove return true的时候调用(一般不用我们自己处理,直接super就好)
*/
public void onMoved(final RecyclerView recyclerView,
final RecyclerView.ViewHolder viewHolder,
int fromPos,
final RecyclerView.ViewHolder target,
int toPos,
int x,
int y) {
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y);
}
/**
* 针对swipe和drag状态,当一个item view在swipe、drag状态结束的时候调用
* drag状态:当手指释放的时候会调用
* swipe状态:当item从RecyclerView中删除的时候调用,一般我们会在onSwiped()函数里面删除掉指定的item view
*/
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
}
/**
* 针对swipe和drag状态,整个过程中一直会调用这个函数,随手指移动的view就是在super里面做到的(和ItemDecoration里面的onDraw()函数对应)
*/
public void onChildDraw(Canvas c,
RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
float dX,
float dY,
int actionState,
boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
/**
* 针对swipe和drag状态,整个过程中一直会调用这个函数(和ItemDecoration里面的onDrawOver()函数对应)
* 这个函数提供给我们可以在RecyclerView的上面再绘制一层东西,比如绘制一层蒙层啥的
*/
public void onChildDrawOver(Canvas c,
RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
float dX,
float dY,
int actionState,
boolean isCurrentlyActive) {
super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
/**
* 针对swipe和drag状态,当手指离开之后,view回到指定位置动画的持续时间(swipe可能是回到原位,也有可能是swipe掉)
*/
public long getAnimationDuration(RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
return super.getAnimationDuration(recyclerView, animationType, animateDx, animateDy);
}
/**
* 针对drag状态,当itemView滑动到RecyclerView边界的时候(比如下面边界的时候),RecyclerView会scroll,
* 同时会调用该函数去获取scroller距离(不用我们处理 直接super)
*/
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView,
int viewSize,
int viewSizeOutOfBounds,
int totalSize,
long msSinceStartScroll) {
return super.interpolateOutOfBoundsScroll(recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
}
有了对ItemTouchHelper.Callback里面函数的了解,我们来看一看ItemTouchHelper的使用步骤,我们分为四个步骤:
只有当我们在对ItemTouchHelper源码有了一个大概的了解之后,才会让我们更好的理解ItemTouchHelper使用。
在ItemTouchHelper源码走读之前,我们先抛出几个疑问:
现在开始进入正题,开始对ItemTouchHelper代码进行走读。
ItemTouchHelper源码所有的的逻辑处理都是围绕RecyclerView使的四个类来进行,分别是:ItemDecoration、OnItemTouchListener、OnChildAttachStateChangeListener、GestureDetector。ItemTouchHelper里面所有的逻辑处理都是围绕着四个类来进行的。所以我们先对这四个帮助类做一个简单的介绍,关于这几个类更加详细的解释可以自行去google。
ItemDecoration:用来装饰RecyclerView中每个item的帮助类。ItemDecoration里面就三个函数:getItemOffsets()用来给每个item设置额外的offset、onDraw()可以通过这个函数给item绘制任何合适的decoration装饰、onDrawOver()也是用来给item绘制装饰用的但是它和onDraw()函数还是有区别的;onDrawOver()是在RecyclerView的draw()调用完之后在调用的,换句话说onDrawOver()是绘制在最上层的。ItemTouchHelper源码里面我们会在ItemDecoration的onDraw()里面让item随手指移动。之前的博客我们也使用ItemDecoration实现了一个简单的功能,有兴趣的可以瞧下RecyclerView分组悬浮列表
OnItemTouchListener:RecyclerView提供给我们处理item各种事件的一个类,一般会配合GestureDetector来处理item的各种手势事件。或者用来处理item里面一些事件的拦截。OnItemTouchListener里面有三个大家非常熟悉的函数:onInterceptTouchEvent()、onTouchEvent()、onRequestDisallowInterceptTouchEvent()。ItemTouchHelper源码里面我们会在OnItemTouchListener里面处理item的各种事件。
OnChildAttachStateChangeListener:用来监听RecyclerView里面item添加和删除。当我们上层逻辑有item删除的时候,ItemTouchHelper会在OnChildAttachStateChangeListener的onChildViewDetachedFromWindow()函数里面做一些回收工作的处理。
GestureDetector:GestureDetector用来获取触摸过程中的各种手势事件。ItemTouchHelper源码里面会用到GestureDetector来获取item长按的的手势,长按之后然后判断要不要进入drag状态。
ItemTouchHelper select()函数,进入退出swipe或者drag状态的时候会调用到select()函数。
我们先来看下ItemTouchHelper里面的select()函数,首先select()函数两个参数:ViewHolder代表当前swipe或者drag选中的item对应的ViewHolder(如果ViewHolder不为空代表选中了一个item,为空代表swipe或者drag释放了item)、actionState代表当前模式有三个值ACTION_STATE_IDLE空闲状态、ACTION_STATE_SWIPE状态对应 swipe模式、ACTION_STATE_DRAG状态对应 drag模式。select()函数里面做的主要工作有:
如果之前mSelected不会空的时候,会给该mSelected对应的item设置动画,这个动画主要用来处理这些情况的,比如是swipe模式对应的mSelected的时候,当手指释放的时候该mSelected对应的item要么是回到原始位置,要么是滑出屏幕之外。这些都是通过动画来完成的。如果是drag模式对应的mSelected的时候,同样当手指释放的时候该mSelected对应的item也要回到指定的位置上去。这些动画都会存放在mRecoverAnimations里面。
把当前参数的ViewHolder设置给mSelected,同时记录mSelectedStartX,mSelectedStartY等的一些位置,便于后面计算移动的距离。
调用Callback的onSelectedChanged()函数。可以通过参数ViewHolder是否为空来判断是进入还是退出swipe或者drag状态。
告诉RecyclerView重绘,强迫RecyclerView去调用onDraw()函数。(RecyclerView的onDraw()函数的调用会引起ItemDecoration里面onDraw()函数的调用)
简单的看了下select()函数,之后,我们再来从头来梳理下ItemTouchHelper逻辑的流程。
attachToRecyclerView()函数相关逻辑处理
第一步,从attachToRecyclerView()函数开始,该函数里面setupCallbacks()的调用给RecyclerView设置ItemDecoration、OnItemTouchListener、OnChildAttachStateChangeListener、GestureDetector的一些处理。(会在ItemDecoration里面onDraw()函数里面处理item随手指移动的逻辑,OnItemTouchListener里面处理item事件拦截和事件处理的逻辑,OnChildAttachStateChangeListener里面处理当上层逻辑删除item的时候一些回收机制的逻辑,GestureDetector里面处理item长按的逻辑)。
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.addOnChildAttachStateChangeListener(this);
initGestureDetector();
}
OnItemTouchListener相关逻辑处理
第二步,因为ItemTouchHelper里面所的逻辑都是围绕触摸事件来进行的,所以当MotionEvent 事件没有被item的子view处理的时候,该MotionEvent 事件会进入到RecyclerView的帮助类OnItemTouchListener里面去,所以这一步的重点就转到了OnItemTouchListener里面的逻辑处理部分了
private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
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) {
mActivePointerId = event.getPointerId(0);
mInitialTouchX = event.getX();
mInitialTouchY = event.getY();
obtainVelocityTracker();
if (mSelected == null) {
final RecoverAnimation animation = findAnimation(event);
if (animation != null) {
mInitialTouchX -= animation.mX;
mInitialTouchY -= animation.mY;
endRecoverAnimation(animation.mViewHolder, true);
if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
mCallback.clearView(mRecyclerView, animation.mViewHolder);
}
select(animation.mViewHolder, animation.mActionState);
updateDxDy(event, mSelectedFlags, 0);
}
}
} else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
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
final int index = event.findPointerIndex(mActivePointerId);
if (DEBUG) {
Log.d(TAG, "pointer index " + index);
}
if (index >= 0) {
checkSelectForSwipe(action, event, index);
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return mSelected != null;
}
@Override
public void onTouchEvent(RecyclerView recyclerView, 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) {
checkSelectForSwipe(action, event, activePointerIndex);
}
ViewHolder viewHolder = mSelected;
if (viewHolder == null) {
return;
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
if (activePointerIndex >= 0) {
updateDxDy(event, mSelectedFlags, activePointerIndex);
moveIfNecessary(viewHolder);
mRecyclerView.removeCallbacks(mScrollRunnable);
mScrollRunnable.run();
mRecyclerView.invalidate();
}
break;
}
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
// fall through
case MotionEvent.ACTION_UP:
select(null, ACTION_STATE_IDLE);
mActivePointerId = ACTIVE_POINTER_ID_NONE;
break;
case MotionEvent.ACTION_POINTER_UP: {
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;
}
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (!disallowIntercept) {
return;
}
select(null, ACTION_STATE_IDLE);
}
};
看到里面有三个我们非常熟悉的函数:onInterceptTouchEvent()、onTouchEvent()、onRequestDisallowInterceptTouchEvent()。其中一个用来处理拦截事件的逻辑,一个用来处理事件逻辑,最后一个用来给子view设置item是否可以拦截的设置。
onInterceptTouchEvent()函数里面的逻辑细节
总的来就是如果有mSelected的时候事件就会被拦截下来(mSelected是会随手指移动的item对应的ViewHolder)。onInterceptTouchEvent()函数里面更加具体的细节先把事件添加到GestureDetector里面去(便于GestureDetector里面获取不同的手势的处理),然后分别对MotionEvent.ACTION_DOWN、 MotionEvent.ACTION_UP做不同的逻辑处理;MotionEvent.ACTION_DOWN里面会先记录下初始按下的位置,接下来如果当前触摸位置对应的item有动画(不管是swipe还是drag模式,在手指离开的时候,当前选中的item都会有一个到指定位置的动画)还在执行动画中。这个时候这个item会当做选中的item来处理。MotionEvent.ACTION_UP里面就是清除之前mSelected的选择。
onTouchEvent()函数里面,
- checkSelectForSwipe(action, event, activePointerIndex)的调用里面会先去判断是否支持swipe模式Callback.isItemViewSwipeEnabled(),然后去判断swipe支持的方向是否和滑动的方向是否一致Callback.getAbsoluteMovementFlags()。如果这两个条件都满足会在select(vh, ACTION_STATE_SWIPE)函数里面把当前手指下对应的item设置为mSelected,模式对应设置为ACTION_STATE_SWIPE swipe模式。
- MotionEvent.ACTION_MOVE的时候先调用updateDxDy(event, mSelectedFlags, activePointerIndex)更新已经滑动的距离,接着调用moveIfNecessary(viewHolder)去设置是否要move,里面也会去回调Callback里面的chooseDropTarget()、onMoved()的函数。接着调用RecyclerView的invalidate()函数迫使RecyclerView去调用onDraw()函数。
- MotionEvent.ACTION_UP的时候就是调用了select(null, ACTION_STATE_IDLE)做一些释放操作。
onRequestDisallowInterceptTouchEvent函数里面
如果子view设置disallow的时候会调用select(null, ACTION_STATE_IDLE)函数,其实也好理解,子view都告诉父view不能处理这个事件饿,所以要做一些释放操作。
GestureDetectorCompat类的使用
OnItemTouchListener的帮助类mOnItemTouchListener里面我们看到到了swipe模式的进入时机(onTouchEvent()函数里面checkSelectForSwipe()函数的调用)。但是没有看到drag模式是怎么进入的呀,别着急,另一个帮助类登场了;GestureDetectorCompat。GestureDetectorCompat里面用到的关键的东西都在ItemTouchHelperGestureListener类里面呢:
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
ItemTouchHelperGestureListener() {
}
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public void onLongPress(MotionEvent e) {
View child = findChildView(e);
if (child != null) {
ViewHolder vh = mRecyclerView.getChildViewHolder(child);
if (vh != null) {
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);
}
if (mCallback.isLongPressDragEnabled()) {
select(vh, ACTION_STATE_DRAG);
}
}
}
}
}
}
咦,就处理了一个onLongPress长按手势事件,里面逻辑就是先得到当前MotionEvent事件对应的ViewHolder,调用Callback的hasDragFlag()函数判断是否允许进入drag模式,在调用Callback的isLongPressDragEnabled()函数判断是否允许长按进入drag模式,最后调用select(vh, ACTION_STATE_DRAG)设置mSelected。之后MotionEvent移动事件的处理就都跑到OnItemTouchListener帮助类里面的onTouchEvent()函数里面去了。
通过上面对OnItemTouchListener和手势帮助类GestureDetectorCompat的分析我们可以知道:
ItemDecoration类的使用
分析到这个时候,咱们还没看到当进入swipe或者drag模式之后,mSelected对应的item是怎么随着手指移动的呀。这个时候就是ItemDecoration派上用场的时候了。上面触摸移动的过程中一直会调用mRecyclerView.invalidate()函数,迫使RecyclerView的onDraw()函数的调用。RecyclerView的onDraw()的调用又会引起ItemDecoration里面的onDraw()函数的调用。瞧一瞧
@Override
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];
}
mCallback.onDraw(c, parent, mSelected,
mRecoverAnimations, mActionState, dx, dy);
}
又跑到Callback里面的onDraw()函数去了,在更进去瞧一瞧,
void onDraw(Canvas c, RecyclerView parent, ViewHolder selected,
List recoverAnimationList,
int actionState, float dX, float dY) {
final int recoverAnimSize = recoverAnimationList.size();
for (int i = 0; i < recoverAnimSize; i++) {
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();
onChildDraw(c, parent, selected, dX, dY, actionState, true);
c.restoreToCount(count);
}
}
该函数里面分两部分,一部分是动画列表里面对应item的绘制,另一部分就是swipe或者drag模式选中的item的绘制。也好理解动画过程中的那些view要更新位置,随跟随手指移动的view也要更新位置。两个都是调用了onChildDraw()函数,最终到了ItemTouchUIUtilImpl里面BaseImpl类的
@Override
public void onDraw(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
view.setTranslationX(dX);
view.setTranslationY(dY);
}
到这里我们就分析完了item随手指移动的逻辑了。
总结下随手指移动的逻辑,在手指移动的过程中会一直调用mRecyclerView.invalidate(),迫使RecyclerView去调用onDraw(),接着调用到ItemDecoration里面的onDraw(),又调用到Callback里面的onDraw(),接着又到Callback里面的onChildDraw()函数。最终到了ItemTouchUIUtilImpl内部BaseImpl类的onDraw()函数里面最后会调用view.setTranslationX(),view.setTranslationY()来移动view。
这里你可能会提出一个疑问,不对呀。看效果的时候随手指移动的那个item感觉是绘制在RecyclerView之上的呀,因为我们手指滑动的时候选中的item是在RecyclerView的上层滑动的呀。这个是咋做到的呀。这里就要分:Build.VERSION.SDK_INT<21、Build.VERSION.SDK_INT>=21两种情况了。
Build.VERSION.SDK_INT<21:改变了RecyclerView里面item的绘制顺序,把选中的item放到最后一个绘制。详细的内容请参考select()函数里面addChildDrawingOrderCallback()里面具体细节。
Build.VERSION.SDK_INT>=21:通过给选中的item 调用View.setElevation()增加效果来实现的,详细的内容请参考ItemTouchUIUtilImpl内部Api21Impl类onDraw()函数里面具体细节。
ItemTouchHelper源码逻辑看起来也不是很复杂么,上面的分析也只是做了一个非常简单的分析,里面很多细节都是一笔带过没有讲的很清楚。我也希望这次的分析能给大家起到一个抛砖引玉的作用。如果大家有什么疑问或者对ItemTouchHelper使用有哪里不清楚的,欢迎在底下留言。在能力范围之内都会为大家解答的。