ItemTouchHelper源码分析

       ItemTouchHelper是一个强大的帮助类。用来配合RecyclerView使用,ItemTouchHelper同一时刻只能支持两种效果:swipe、drag中的一种。分别用来实现RecyclerView里面item侧滑删除(swipe)效果或者item长按拖拽移动(drag)。当然swipe和drag效果同一时刻只能支持一种。因为事件冲突不能同时支持。

一、ItemTouchHelper效果展示

drag实例效果

swipe实例效果

实例代码下载地址

二、ItemTouchHelper使用的简单介绍

       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<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.Callback里面函数的了解,我们来看一看ItemTouchHelper的使用步骤,我们分为四个步骤:

  1. 一定要有RecyclerView(ItemTouchHelper一定要配合RecyclerView使用)。
  2. 新建一个类继承自ItemTouchHelper.Callback(也可以是ItemTouchHelper.SimpleCallback)。我们主要在这个类的回调函数中处理swipe或者drag过程中我们自己需要处理的一些逻辑。比如,在getMovementFlags()函数里面设置swipe或者drag支持的方向(上下左右)、在onSwiped()函数里面删除掉需要swipe侧滑删除item的逻辑、在onMove()函数里面处理drag拖拽移动变换位置的逻辑等等。
  3. 新建ItemTouchHelper对象,参数正好是第二步继承自ItemTouchHelper.Callback的类。
  4. ItemTouchHelper.attachToRecyclerView(RecyclerView) 把ItemTouchHelper关联到RecyclerView上去,之后所有的处理都会归总到第二步骤继承自ItemTouchHelper.Callback的类中。

三、ItemTouchHelper源码走读

       只有当我们在对ItemTouchHelper源码有了一个大概的了解之后,才会让我们更好的理解ItemTouchHelper使用。

       在ItemTouchHelper源码走读之前,我们先抛出几个疑问:

  1. RecyclerView的触摸事件是怎么在ItemTouchHelper里面进行处理的。
  2. ItemTouchHelper是里面怎么判断进入swipe或者drag状态。
  3. 进入swipe或者drag状态之后,选中的item是怎么随着手指移动的。
  4. 随手指移动的item在移动的时候感觉是在RecyclerView上面移动的,当和item有重叠的时候是怎么让选中的item在上层移动的。

       现在开始进入正题,开始对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()函数里面,

    1. checkSelectForSwipe(action, event, activePointerIndex)的调用里面会先去判断是否支持swipe模式Callback.isItemViewSwipeEnabled(),然后去判断swipe支持的方向是否和滑动的方向是否一致Callback.getAbsoluteMovementFlags()。如果这两个条件都满足会在select(vh, ACTION_STATE_SWIPE)函数里面把当前手指下对应的item设置为mSelected,模式对应设置为ACTION_STATE_SWIPE swipe模式。
    2. MotionEvent.ACTION_MOVE的时候先调用updateDxDy(event, mSelectedFlags, activePointerIndex)更新已经滑动的距离,接着调用moveIfNecessary(viewHolder)去设置是否要move,里面也会去回调Callback里面的chooseDropTarget()、onMoved()的函数。接着调用RecyclerView的invalidate()函数迫使RecyclerView去调用onDraw()函数。
    3. 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的分析我们可以知道:

  1. swipe和drag模式进入的时机。swipe模式进入的判断是在OnItemTouchListener帮助类里面onTouchEvent()的函数的checkSelectForSwipe()的调用里面判断是否进入,drag模式进入的判断是在GestureDetectorCompat帮助里ItemTouchHelperGestureListener里面onLongPress()里面判断是否进入。
  2. 触摸事件在移动的过程中(信息信息请看OnItemTouchListener帮助类里面onTouchEvent()函数的MotionEvent.ACTION_MOVE逻辑处理)会一直去更新滑动的位置(updateDxDy函数)和一直让去重绘(mRecyclerView.invalidate的调用)。

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<ItemTouchHelper.RecoverAnimation> 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使用有哪里不清楚的,欢迎在底下留言。在能力范围之内都会为大家解答的。