RecyclerView源码分析之触控与拖拽

917 阅读13分钟

一直觉得这部分很复杂没有去深入研究,但每次都逃避不是办法,决定克服它,回过头来看,不管看起来多复杂的代码逻辑,其实都是服务于业务,我们抓住关键的业务逻辑来理解代码逻辑,就能融会贯通了。

一、核心问题

这次我们核心问题是弄清楚以下几个疑问:

  • 事件的监听?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…