一直觉得这部分很复杂没有去深入研究,但每次都逃避不是办法,决定克服它,回过头来看,不管看起来多复杂的代码逻辑,其实都是服务于业务,我们抓住关键的业务逻辑来理解代码逻辑,就能融会贯通了。
一、核心问题
这次我们核心问题是弄清楚以下几个疑问:
- 事件的监听?drag和swipe触发?
- 拖拽未落位的空白占位图如何产生?跟手的那个View如何产生?如何跟手?
- 如何在移动过程中不断找到换位目标并换位?
- 拖拽到底部边缘后如何触发列表自动往上滚?
二、简单使用回顾
使用步骤
1.新建一个类继承自ItemTouchHelper.Callback(也可以是ItemTouchHelper.SimpleCallback)。
我们主要在这个类的回调函数中处理swipe或者drag过程中我们自己需要处理的一些逻辑。比如,在getMovementFlags()函数里面设置swipe或者drag支持的方向(上下左右)、在onSwiped()函数里面删除掉需要swipe侧滑删除item的逻辑、在onMove()函数里面处理drag拖拽移动变换位置的逻辑等等
2.新建ItemTouchHelper对象,参数正好是第二步继承自ItemTouchHelper.Callback的类
3.ItemTouchHelper.attachToRecyclerView(RecyclerView) 把ItemTouchHelper关联到RecyclerView上去
Callback类
简介
ItemTouchHelper最关键的点其实就是ItemTouchHelper.Callback回调类的使用,上层所有的关键逻辑操作都是在CallBack回调类中实现, 除了常规的我们需要实现的onMove实现数据交换,onSwiped实现数据删除以外。我们还可以通过重写很多其他的方法实现相关逻辑的定制修改, 比如修改触发拖拽和滑动删除的条件,修改悬浮跟随的View逻辑,修改如何根据坐标寻找目标View,修改目标View的是否可交换的条件(DrapOver)等等。
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滑出屏幕动画结束的时候postDispatchSwipe中调用
*/
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<RecyclerView.ViewHolder> 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和RecyclerView
根据使用步骤我们先看一下绑定TouchHelper和RecyclerView的时候的代码
ItemTouchHelper mItemTouchHelper = new ItemTouchHelper(new ItemTouchHelperCallback()); mItemTouchHelper.attachToRecyclerView(mRecyclerView);
<!--ItemTouchHelper.java
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
...
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
...
setupCallbacks();
}
}
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);//将ItemTouchHelper实现的ItemDecoration接口注册到RecyclerView,我们记住这个Decoration会在ReyclerView重绘的时候被调用到,用于控制跟手的View的位移
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);//通过OnItemTouchListener注册到RecyclerView,用于接管分发到RecyclerView的事件,其中swipe就在这里触发,并且move检测也在这里完成
mRecyclerView.addOnChildAttachStateChangeListener(this);//将ItemTouchHelper实现的OnChildAttachStateChangeListener接口注册到RecyclerView,接口里有两个方法,分别是在RecycleView添加一个View与删除一个View的时候回调
startGestureDetection();//借助GestureDetector设置手势监听,其中拖拽的触发就是在这里面触发的
}
private void startGestureDetection() {
mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
mItemTouchHelperGestureListener);
}
-->
上面代码主要做了这几件事
1.RecyclerView和ItemTouchHelper只能绑定一次
2.将ItemTouchHelper实现的ItemDecoration接口注册到RecyclerView,我们记住这个Decoration会在ReyclerView重绘的时候被调用到,用于控制跟手的View的位移
3.通过OnItemTouchListener注册到RecyclerView,用于接管分发到RecyclerView的事件,其中swipe就在这里触发,并且move检测也在这里完成
4.将ItemTouchHelper实现的OnChildAttachStateChangeListener接口注册到RecyclerView,接口里有两个方法,分别是在RecycleView添加一个View与删除一个View的时候回调
5.借助GestureDetector设置手势监听,其中拖拽的触发就是在这里面触发的
OnItemTouchListener接管触摸事件
ItemTouchHelper里面所的逻辑都是围绕触摸事件来进行的,触摸事件的入口就是OnItemTouchListener接口的三个我们非常熟悉的函数: onInterceptTouchEvent()、onTouchEvent()、onRequestDisallowInterceptTouchEvent()。
其中一个用来处理拦截事件的逻辑,一个用来处理事件逻辑,最后一个用来给子view设置item是否可以拦截的设置,接下来依次分析三个方法的逻辑。
<!--ItemTouchHelper.java
private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
@NonNull MotionEvent event) {
mGestureDetector.onTouchEvent(event);//给前面注册的GestureDetector添加监听
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);//记录激活的触控点Id
mInitialTouchX = event.getX();
mInitialTouchY = event.getY();//记录初始位置,后续计算移动的差值Dy、Dx会用到
obtainVelocityTracker();//初始化触摸速度跟踪类VelocityTracker
if (mSelected == null) {
final RecoverAnimation animation = findAnimation(event);//根据当前的MotionEvent查找RecoverAnimation对象
if (animation != null) {
mInitialTouchX -= animation.mX;
mInitialTouchY -= animation.mY;
endRecoverAnimation(animation.mViewHolder, true);//删除RecoverAnimation
if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
mCallback.clearView(mRecyclerView, animation.mViewHolder);
}
select(animation.mViewHolder, animation.mActionState);
updateDxDy(event, mSelectedFlags, 0);//更新Dx与Dy
}
}
} else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mActivePointerId = ACTIVE_POINTER_ID_NONE;
select(null, ACTION_STATE_IDLE);//在CACEL和UP的时候调用select方法取消选中目标
} 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) {//index >= 0 表示最少有一个触控点存在
checkSelectForSwipe(action, event, index);//检查是否满足Swipe条件并选择目标
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);//将事件发给速度跟踪器
}
return mSelected != null;
}
@Override
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) {
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);//检查是否要move,里面也会去回调Callback里面的chooseDropTarget()、onMoved()的函数,进行数据实时换位
mRecyclerView.removeCallbacks(mScrollRunnable);
mScrollRunnable.run();//边缘检查,并决定RecyclerView是否要滚动显示更多内容
mRecyclerView.invalidate();//一直重绘的目的是为了触发RecyclerVie的onDraw调用,然后调用不同ItemDecoration触发装饰器的绘制,包括让跟手的View设置偏移,设置Item的边距等
}
break;
}
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
// fall through
case MotionEvent.ACTION_UP:
select(null, ACTION_STATE_IDLE);//每次状态切换都会经过两次调用,此处对应第二次select,触发动画开始
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主要是干了这几个事情:
1.记录激活的触控点Id
2.记录初始位置,后续计算移动的差值Dy、Dx会用到
3.初始化触摸速度跟踪类VelocityTracker
4.在CANCEL和UP的时候调用select方法取消选中目标
5.如果触控点有效则进入checkSelectForSwipe检测是否触发Swipe
,checkSelectForSwipe会先去判断是否支持swipe模式Callback.isItemViewSwipeEnabled(),然后去Callback.getAbsoluteMovementFlags()判断swipe支持的方向是否和滑动的方向是否一致。如果这两个条件都满足会在select(vh, ACTION_STATE_SWIPE)函数里面把当前手指下对应的item设置为mSelected,模式对应设置为ACTION_STATE_SWIPE swipe模式
onTouchEvent主要是干了这几个事情:
1.再次调用checkSelectForSwipe检测
2.在ACTION_MOVE中执行updateDxDy更新已经滑动的距离
3.在ACTION_MOVE中调用moveIfNecessary检查是否要move,里面也会去回调Callback里面的chooseDropTarget()、onMoved()的函数,进行数据实时换位
4.在ACTION_MOVE中调用ScrollRunnable边缘检查,并决定RecyclerView是否要滚动显示更多内容
5.在ACTION_MOVE中调用RecyclerView的invalidate()函数迫使RecyclerView去调用onDraw()函数,然后调用不同ItemDecoration触发装饰器的绘制,包括让跟手的View设置偏移,设置Item的边距等
6.在ACTION_UP中执行第二次select,触发动画开始,每次状态切换select都会经过两次调用,一次为选中,一次为释放,这里后续讲select会提到
onRequestDisallowInterceptTouchEvent干的事情:
1.子view都告诉父view不能拦截处理这个事件,子View需要自己处理,因此需要做一些释放操作
GestureDetector接管长按触发Drag
GestureDetector接管长按触发Drag
<!--
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
...
@Override
public void onLongPress(MotionEvent e) {
if (!mShouldReactToLongPress) {
return;
}
View child = findChildView(e);
if (child != null) {
ViewHolder vh = mRecyclerView.getChildViewHolder(child);
if (vh != null) {
if (!mCallback.hasDragFlag(mRecyclerView, vh)) {//hasDragFlag检查滑动方向是否满足Callback设置的Drag的监听方向
return;
}
...
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()) {//使用Callback判断是否允许长按进入Drag
select(vh, ACTION_STATE_DRAG);//触发进入Drag
}
}
}
}
}
}
-->
这里完成的事情很简单:
1.hasDragFlag检查滑动方向是否满足Callback设置的Drag的监听方向
2.使用Callback判断是否允许长按进入Drag
3.上诉都满足的话则调用select()触发进入Drag
scrollIfNecessary与moveIfNecessary
scrollIfNecessary
scrollIfNecessary作用是就是检查是否有必要滚动RecyclerView,用于在拖拽状态下主动滑动列表显示更多内容进行交换
<!--ItemTouchHelper.java
final Runnable mScrollRunnable = new Runnable() {
@Override
public void run() {
if (mSelected != null && scrollIfNecessary()) {//scrollIfNecessary的作用是检测我们滑动是否到达RecycleView的边缘区域,如果到达边缘区域则将RecycleView移动(scrollBy)
if (mSelected != null) { //it might be lost during scrolling
moveIfNecessary(mSelected);
}
mRecyclerView.removeCallbacks(mScrollRunnable);
ViewCompat.postOnAnimation(mRecyclerView, this);
}
}
};
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) {
final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom
- (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom());
if (bottomDiff > 0) {
scrollY = bottomDiff;
}
}
}
...
if (scrollY != 0) {
scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView,//调用了callback的interpolateOutOfBoundsScroll方法,以在这里监听到我们拖出视图边界的调用
mSelected.itemView.getHeight(), scrollY,
mRecyclerView.getHeight(), scrollDuration);
}
if (scrollX != 0 || scrollY != 0) {
if (mDragScrollStartTimeInMs == Long.MIN_VALUE) {
mDragScrollStartTimeInMs = now;
}
mRecyclerView.scrollBy(scrollX, scrollY);
return true;
}
mDragScrollStartTimeInMs = Long.MIN_VALUE;
return false;
}
-->
scrollIfNecessary完成的事情包括:
1.根据目前的Dy计算出最新的目标位置curY
2.根据curY以及目标View的高度以及装饰器的高度,这个高度如果在超出边缘后,理论上来说或大于RecyclerView的Bottom,因此算出和屏幕底部的的Bottom位置之间的差值,这个差值就是屏幕需要向上滚的值
3.调用mRecyclerView.scrollBy执行滚动
moveIfNecessary
moveIfNecessary是实现换位的关键,内部会检查是否需要进行数据实时换位,需要的话就调用Callback.onMove,然后触发交换动画
<!--ItemTouchHelper.java
void moveIfNecessary(ViewHolder viewHolder) {
...
if (mActionState != ACTION_STATE_DRAG) {//只有drag状态才需要检测
return;
}
final float threshold = mCallback.getMoveThreshold(viewHolder);//根据Callback拿到Move的阀值,可以重写来自定义用户视为拖动的距离
final int x = (int) (mSelectedStartX + mDx);
final int y = (int) (mSelectedStartY + mDy);
if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold//当移动距离小于拖动距离,return掉
&& Math.abs(x - viewHolder.itemView.getLeft())
< viewHolder.itemView.getWidth() * threshold) {
return;
}
List<ViewHolder> swapTargets = findSwapTargets(viewHolder);//寻找可能会交换位置的ItemView
...
// may swap.
ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);//找到符合条件交换的ItemView,这里条件比findSwapTargets更严苛
...
final int fromPosition = viewHolder.getAdapterPosition();
if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
// keep target visible
mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
target, toPosition, x, y);
}
}
-->
moveIfNecessary核心事项:
1.剔除不符合move的条件直接返回,包括根据Callback拿到Move的阀值,当移动距离小于拖动距离直接返回跳过
2.findSwapTargets寻找可能会交换位置的ItemView
3.chooseDropTarget找到符合条件交换的ItemView
4.Callback.onMoved实现真正的换位,这个方法须要咱们手动实现!!
详细看一下 findSwapTargets:
<!--
private List<ViewHolder> findSwapTargets(ViewHolder viewHolder) {
if (mSwapTargets == null) {
mSwapTargets = new ArrayList<>();
mDistances = new ArrayList<>();
} else {
mSwapTargets.clear();
mDistances.clear();
}
final int margin = mCallback.getBoundingBoxMargin();//可以通过Callback控制覆盖命中的规则,Margin越大越容易命中,让重叠更加容易发生
final int left = Math.round(mSelectedStartX + mDx) - margin;
final int top = Math.round(mSelectedStartY + mDy) - margin;
final int right = left + viewHolder.itemView.getWidth() + 2 * margin;
final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin;
final int centerX = (left + right) / 2;
final int centerY = (top + bottom) / 2;
final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
final int childCount = lm.getChildCount();
for (int i = 0; i < childCount; i++) {
View other = lm.getChildAt(i);
if (other == viewHolder.itemView) {
continue; //myself!
}
if (other.getBottom() < top || other.getTop() > bottom
|| other.getRight() < left || other.getLeft() > right) {
continue;
}
final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other);
if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) {//Callback.canDropOver可以定制目标Item是否可交换,例如负一屏主界面就是通过这个来控制某些卡片无法被交换的,比如轮播和快捷功能等
// find the index to add
final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2);
final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2);
final int dist = dx * dx + dy * dy;
int pos = 0;
final int cnt = mSwapTargets.size();
for (int j = 0; j < cnt; j++) {
if (dist > mDistances.get(j)) {
pos++;
} else {
break;
}
}
mSwapTargets.add(pos, otherVh);
mDistances.add(pos, dist);
}
}
return mSwapTargets;
}
-->
findSwapTargets核心事项包括:
1.获取命中Margin,通过Callback控制覆盖命中的规则,Margin越大越容易命中,让重叠更加容易发生
2.计算重叠的区域
3.遍历所有的子View并通过重叠命中的区域筛选后记录到mSwapTargets
findSwapTargets寻找可能交互的目标的View集合的规则为:
只要选中的ItemView跟某一个ItemView重叠,那么这个ItemView可能会跟选中的ItemView交换位置
详细看一下 chooseDropTarget:
dropTargets代表的是待交换的目标位置
<!--Callback
public ViewHolder chooseDropTarget(@NonNull ViewHolder selected,
@NonNull List<ViewHolder> dropTargets, int curX, int curY) {
int right = curX + selected.itemView.getWidth();
int bottom = curY + selected.itemView.getHeight();
ViewHolder winner = null;
int winnerScore = -1;
final int dx = curX - selected.itemView.getLeft();
final int dy = curY - selected.itemView.getTop();
final int targetsSize = dropTargets.size();
for (int i = 0; i < targetsSize; i++) {
final ViewHolder target = dropTargets.get(i);
...
if (dy < 0) {
int diff = target.itemView.getTop() - curY;
if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) {
final int score = Math.abs(diff);
if (score > winnerScore) {
winnerScore = score;
winner = target;
}
}
}
if (dy > 0) {
int diff = target.itemView.getBottom() - bottom;
if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) {
final int score = Math.abs(diff);
if (score > winnerScore) {
winnerScore = score;
winner = target;
}
}
}
}
return winner;
}
-->
chooseDropTarget核心事项包括:
1.从可能的目标列表中找到符合条件交换的ItemView
select()目标选中
作用是在状态切换的时候,记录选择的ViewHolder,并在取消选择的时候初始化并开始Drag或者Swipe动画,每次状态切换都会经过两次调用:
1.第一次在我们选中的时候,selected不为null,作用是确定我们手指选择的View
2.第二次在我们手指放开的时候,selected为null,作用是给这个View设置动画,并且执行
<!--
void select(@Nullable ViewHolder selected, int actionState) {
if (selected == mSelected && actionState == mActionState) {
return;
}
...
int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
- 1;
boolean preventLayout = false;
if (mSelected != null) {//mSelected不为空,表示对应那次状态切换的第二次进入
final ViewHolder prevSelected = mSelected;
if (prevSelected.itemView.getParent() != null) {
final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0
: swipeIfNecessary(prevSelected);
releaseVelocityTracker();
// find where we should animate to
final float targetTranslateX, targetTranslateY;
int animationType;
...
if (prevActionState == ACTION_STATE_DRAG) {
animationType = ANIMATION_TYPE_DRAG;
} else if (swipeDir > 0) {
animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
} else {
animationType = ANIMATION_TYPE_SWIPE_CANCEL;
}
getSelectedDxDy(mTmpPosition);
final float currentTranslateX = mTmpPosition[0];
final float currentTranslateY = mTmpPosition[1];
final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
prevActionState, currentTranslateX, currentTranslateY,
targetTranslateX, targetTranslateY) {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (this.mOverridden) {
return;
}
//动画结束如果swipeDir<=0则drag与swipe失败,Callback会调用clearView方法
//swipeDir >0则表示成功,会调用postDispatchSwipe方法
//当为DRAG状态时候因为swipeDir为0,所以只走clearView方法
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.
mPendingCleanup.add(prevSelected.itemView);
mIsPendingCleanup = true;
if (swipeDir > 0) {
// Animation might be ended by other animators during a layout.
// We defer callback to avoid editing adapter during a layout.
postDispatchSwipe(this, swipeDir);
}
}
// removed from the list after it is drawn for the last time
if (mOverdrawChild == prevSelected.itemView) {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
}
}
};
//获取AnimationDuration,我们可以通过重写这个方法来设定动画的时间
final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
rv.setDuration(duration);
mRecoverAnimations.add(rv);
rv.start();
preventLayout = true;
} else {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
mCallback.clearView(mRecyclerView, prevSelected);
}
mSelected = null;
}
//当传进来的selected不为空的时候将selected赋给mSelected,selected为空,表示对应那次状态切换的第一次进入
if (selected != null) {
mSelectedFlags =
(mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask)
>> (mActionState * DIRECTION_FLAG_COUNT);
mSelectedStartX = selected.itemView.getLeft();
mSelectedStartY = selected.itemView.getTop();
mSelected = selected;
if (actionState == ACTION_STATE_DRAG) {
mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
}
...
mCallback.onSelectedChanged(mSelected, mActionState);//每次select会带来拖动或者滑动的ViewHolder改变,所以这里会调用onSelectedChanged方法,我们可以通过回调接受到这些信息
mRecyclerView.invalidate();
}
-->
四、核心问题回答
根据上面的分析后我们就可以回答最初的几个疑问了
事件的监听?drag和swipe触发?
事件的监听:
是通过在ItemTouchHelper中通过OnItemTouchListener承接了分发到RecyclerView的事件
drag和swipe触发:
swipe模式进入的判断是在OnItemTouchListener帮助类里面onTouchEvent()的函数的checkSelectForSwipe()的调用里面判断是否进入
drag模式进入的判断是在GestureDetectorCompat帮助里ItemTouchHelperGestureListener里面onLongPress()里面判断是否进入
拖拽未落位的空白占位图如何产生?跟手的那个View如何产生?如何跟手?
拖拽未落位的空白占位图如何产生?跟手的那个View如何产生?
不用刻意产生,因为跟手的View并不是一个新的View,只是原View通过translation变换位置的结果
如何跟手?:
围绕ItemDecoration和Callback来实现的。
具体调用逻辑为: 随手指移动的逻辑,在手指移动的过程中会一直调用mRecyclerView.invalidate(),迫使RecyclerView去调用onDraw(),接着调用到ItemDecoration里面的onDraw(),又调用到Callback里面的onDraw(),接着又到Callback里面的onChildDraw()函数。最终到了ItemTouchUIUtilImpl内部BaseImpl类的onDraw()函数里面最后会调用view.setTranslationX(),view.setTranslationY()来移动view
<!--RecyclerView.java
@Override
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);
}
}
-->
由于ItemTouchHelper实现了ItemDecoration接口,因此上诉会调用到ItemTouchHelper.onDraw
<!--ItemTouchHelper.java
@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会调用到ItemTouchUIUtilImpl.onDraw
<!--ItemTouchUIUtilImpl.java
public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
int actionState, boolean isCurrentlyActive) {
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更后绘制,显得是悬浮在RecyclerView上面一样的效果
view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
}
}
}
view.setTranslationX(dX);
view.setTranslationY(dY);
}
-->
如何在移动过程中不断找到换位目标并换位?
在ACTION_MOVE的过程中,会不断的调用moveIfNecessary来找到目标交换位置,并且调用Callback.onMove实现位置交换。
拖拽到底部边缘后如何触发列表自动往上滚?
在ACTION_MOVE的过程中,会不断的调用scrollIfNecessary完成:
1.根据目前的Dy计算出最新的目标位置curY
2.根据curY以及目标View的高度以及装饰器的高度,这个高度如果在超出边缘后,理论上来说或大于RecyclerView的Bottom,因此算出和屏幕底部的的Bottom位置之间的差值,这个差值就是屏幕需要向上滚的值
3.调用mRecyclerView.scrollBy执行滚动
五、参考资料
【RecyclerView 扩展(二) - 手把手教你认识ItemTouchHelper】
www.javashuo.com/article/p-c…
【RecyclerView 源码分析(十)ItemTouchHelper源码详解】
blog.csdn.net/aha_jasper/…
【ItemTouchHelper(二)源码简析】
www.jianshu.com/p/b8e45aa3a…