RecyclerView 扩展(二) - 手把手教你认识ItemTouchHelper

6,260 阅读18分钟

  今天我们来学习一下RecyclerView另一个鲜为人知的辅助类--ItemTouchHelper。我们在做列表视图,就比如说,ListView或者RecyclerView,通常会有两种需求:1. 侧滑删除;2. 拖动交换位置。对于第一种需求使用传统的版本实现还比较简单,我们可以自定义ItemView来实现;而第二种的话,可能就稍微有一点复杂,可能需要重写LayoutManager

  这些办法也不否认是有效的解决方案,但是是否是简单和低耦合性的办法呢?当然不是,踩过坑的同学应该都知道,不管是自定义View还是自定义LayoutManager都不是一件简单的事情,其次,自定义ItemView导致Adapter的通用性降低。这些实现方式都是比较麻烦的。

  而谷歌爸爸真是贴心,知道我们都有这种需求,就小手一抖,随便帮我们实现了一个Helper类,来减轻我们的工作量。这就是ItemTouchHelper的作用。

  本文打算从两个方面来教大家认识ItemTouchHelper类:

  1. ItemTouchHelper的基本使用
  2. ItemTouchHelper的源码分析

  本文参考资料:

  1. RecyclerView高级进阶总结:ItemTouchHelper实现拖拽和侧滑删除
  2. ItemTouchHelper源码分析

1. 概述

  在正式介绍ItemTouchHelper之前,我们先来了解ItemTouchHelper是什么东西。

  从ItemTouchHelper的源码中,我们可以看出来,ItemTouchHelper继承了ItemDecoration,根本上就是一个ItemDecoration。关于ItemDecoration的分析,有兴趣的同学可以参考我的文章:RecyclerView 扩展(一) - 手把手教你认识ItemDecoration

public class ItemTouchHelper extends RecyclerView.ItemDecoration
        implements RecyclerView.OnChildAttachStateChangeListener {
}

  至于为什么ItemTouchHelper会继承ItemDecoration,后面会详细的解释,这里就先卖一下关子。

  然后,我们先来看看ItemTouchHelper实现的效果,让大家有一个直观的体验。

  先是侧滑删除的效果:

  然后是拖动交换位置:
  本文打算从上面两种效果来介绍ItemTouchHelper的使用。

2. ItemTouchHelper的基本使用

  既然是手把手教大家认识ItemTouchHelper,所以自然需要介绍它的的基本使用,现在让我们来看看究竟怎么使用ItemTouchHelper

  在正式介绍ItemTouchHelper的基本使用之前,我们还必须了解一个类--ItemTouchHelper.CallbackItemTouchHelper就是依靠这个类来实现侧滑删除和拖动位置两种效果的,我来看看它。

(1). ItemTouchHelper.Callback

  我们在使用ItemTouchHelper时,必须自定义一个ItemTouchHelper.Callback,我们来了解一下其中比较重要的几个方法。

方法名 作用
getMovementFlags 在此方法里面我们需要构建两个flag,一个是dragFlags,表示拖动效果支持的方向,另一个是swipeFlags,表示侧滑效果支持的方向。在我们的Demo中,拖动执行上下两个方向,侧滑执行左右两个方向,这些操作我们都可以在此方法里面定义。
onMove 当拖动效果已经产生了,会回调此方法。在此方法里面,我们通常会更新数据源,就比如说,一个ItemView从0拖到了1位置,那么对应的数据源也需要更改位置。
onSwiped 当侧滑效果以上产生了,会回调此方法。在此方法里面,我们也会更新数据源。与onMove方法不同到的是,我们在这个方法里面从数据源里面移除相应的数据,然后调用notifyXXX方法就行了。

  对于ItemTouchHelper的基本使用来说,我们只需要了解这三个方法就已经OK了。接下来,我将正式介绍ItemTouchHelper的基本使用。

(2). 基本使用

  首先,我们需要自定义一个ItemTouchHelper.Callback,如下:

public class CustomItemTouchCallback extends ItemTouchHelper.Callback {

    private final ItemTouchStatus mItemTouchStatus;

    public CustomItemTouchCallback(ItemTouchStatus itemTouchStatus) {
        mItemTouchStatus = itemTouchStatus;
    }

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        // 上下拖动
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        // 向左滑动
        int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        // 交换在数据源中相应数据源的位置
        return mItemTouchStatus.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        // 从数据源中移除相应的数据
        mItemTouchStatus.onItemRemove(viewHolder.getAdapterPosition());
    }
}

  然后,我们在使用RecyclerView时,添加这两行代码就行了:

        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new CustomItemTouchCallback(mAdapter));
        itemTouchHelper.attachToRecyclerView(mRecyclerView);

  最终的效果就是上面的动图展示的,是不是觉得非常的简单呢?接下来,我将正式的分析ItemTouchHelper的源码。

(4).源码

  为了方便大家理解,我将我的代码上传到github,有兴趣的同学可以看看:ItemTouchHelperDemo

3. ItemTouchHelper的源码分析

  我们从基本使用中了解到,ItemTouchHelper的使用是非常简单的,所以大家内心有没有一种好奇呢?那就是ItemTouchHelper究竟是怎么实现,为什么两个相对比较复杂的效果,通过几行代码就能实现呢?接下来的内容就能找到答案。

(1). attachToRecyclerView方法

  我们都知道,ItemTouchHelper的入口方法就是attachToRecyclerView方法,接下来,我们先来看看这个方法为我们做了哪些事情。

    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
        mRecyclerView = recyclerView;
        if (recyclerView != null) {
            final Resources resources = recyclerView.getResources();
            mSwipeEscapeVelocity = resources
                    .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
            mMaxSwipeVelocity = resources
                    .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
            setupCallbacks();
        }
    }

    private void setupCallbacks() {
        ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
        mSlop = vc.getScaledTouchSlop();
        mRecyclerView.addItemDecoration(this);
        mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
        mRecyclerView.addOnChildAttachStateChangeListener(this);
        startGestureDetection();
    }

    private void startGestureDetection() {
        mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
        mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
                mItemTouchHelperGestureListener);
    }

  相对来说,attachToRecyclerView方法是比较简单的。这其中,我们发现ItemTouchHelper是通过ItemTouchListener接口来为每个ItemView处理事件,同时,从这里我们可以看出来,在ItemTouchHelper内部还使用了GestureDetector,而这里GestureDetector的作用主要是来判断ItemView是否进行了长按行为。

  ItemTouchHelper的分析重点应该是事件处理,但是在这之前,我们先来看一个方法,这个方法非常的重要的。

(2). select方法

  当我们的操作触发了长按或者侧滑的行为,都会回调此方法,同时当我们手势释放,也会回调此方法。

  所以从大的时机来看,当手势开始或者释放都会回调select方法;而每个大时机又分为两个小时机,分别是长按和侧滑,分别表示拖动交换位置和侧滑删除操作。

  在正式分析select方法的代码之前,我们需要了解两个东西:

  1. selected表示被选中的ViewHolder。其中,selected如果为null,则表示当前处于手势(包括长按和侧滑)释放时机;反之,selected不为null,则表示当前处于手势开始的时机。
  2. actionState表示当前的状态,一共有三个值可选,分别是:1. ACTION_STATE_IDLE表示没有任何手势,此时selected对应的应当是null;2. ACTION_STATE_SWIPE表示当前ItemView处于侧滑状态;3. ACTION_STATE_DRAG表示当前ItemView处于拖动状态。在ItemTouchHelper内部,就是通过这三个状态来判断ItemView处于什么状态。

  接下来我们来看看select方法的代码:

    void select(ViewHolder selected, int actionState) {
        if (selected == mSelected && actionState == mActionState) {
            return;
        }
        mDragScrollStartTimeInMs = Long.MIN_VALUE;
        final int prevActionState = mActionState;
        endRecoverAnimation(selected, true);
        mActionState = actionState;
        // 如果当前是拖动行为,给RecyclerView设置一个ChildDrawingOrderCallback接口
        // 主要是为了调整ItemView绘制的顺序
        if (actionState == ACTION_STATE_DRAG) {
            mOverdrawChild = selected.itemView;
            addChildDrawingOrderCallback();
        }
        int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
                - 1;
        boolean preventLayout = false;
        // 1.手势释放
        if (mSelected != null) {
           // ······
        }
        // 2. 手势开始
        // selected不为null表示手势开始,反之selected为null表示手势释放
        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);
            }
        }
        final ViewParent rvParent = mRecyclerView.getParent();
        if (rvParent != null) {
            rvParent.requestDisallowInterceptTouchEvent(mSelected != null);
        }
        if (!preventLayout) {
            mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
        }
        mCallback.onSelectedChanged(mSelected, mActionState);
        mRecyclerView.invalidate();
    }

  从上面的代码中,我们可以总结出来几个结论:

  1. 如果处于手势开始阶段,即selected不为null,那么会通过getAbsoluteMovementFlags方法来获取执行我们设置的flag,从而就知道执行哪些行为(侧滑或者拖动)和方向(上、下、左和右)。同时还会记录下被选中ItemView的位置。简而言之,就是一些变量的初始化。
  2. 如果处于手势释放阶段,即selected为null,同时mSelected不为null,那么此时需要做的事情就稍微有一点复杂。手势释放之后,需要做的事情无非有两件:1. 相关的ItemView到正确的位置,就比如说,如果滑动条件不满足,那么就返回原来的位置,这个就是一个动画;2. 清理操作,比如说将mSelected重置为null之类的

(3).如何判断一个ItemView是否被选中

  我们知道,一旦调用selected就意味着一个ItemView被选中,接下来的就会随着手势出现侧滑或者拖动的效果了。但是怎么来判断一个ItemView是否被选中,我们从代码来看看,我们分两步来理解:1.侧滑的选中;2. 拖动的选中。

A. 侧滑

  判断侧滑行为是否选中主要在checkSelectForSwipe方法,我们来看看checkSelectForSwipe放大的代码:

    boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
        // 如果mSelected不为null表示已经有ItemView被选中
        // 同时从这里可以看出来Callback的isItemViewSwipeEnabled方法的作用
        if (mSelected != null || action != MotionEvent.ACTION_MOVE
                || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) {
            return false;
        }
        if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
            return false;
        }
        final ViewHolder vh = findSwipedView(motionEvent);
        if (vh == null) {
            return false;
        }
        final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);

        final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
                >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
        // 如果flag没有支持侧滑的方向值,那么返回为false
        if (swipeFlags == 0) {
            return false;
        }

        // mDx and mDy are only set in allowed directions. We use custom x/y here instead of
        // updateDxDy to avoid swiping if user moves more in the other direction
        final float x = motionEvent.getX(pointerIndex);
        final float y = motionEvent.getY(pointerIndex);

        // Calculate the distance moved
        final float dx = x - mInitialTouchX;
        final float dy = y - mInitialTouchY;
        // swipe target is chose w/o applying flags so it does not really check if swiping in that
        // direction is allowed. This why here, we use mDx mDy to check slope value again.
        final float absDx = Math.abs(dx);
        final float absDy = Math.abs(dy);

        if (absDx < mSlop && absDy < mSlop) {
            return false;
        }
        // 这里主要是判断一个滑动是否符合侧滑的条件
        if (absDx > absDy) {
            if (dx < 0 && (swipeFlags & LEFT) == 0) {
                return false;
            }
            if (dx > 0 && (swipeFlags & RIGHT) == 0) {
                return false;
            }
        } else {
            if (dy < 0 && (swipeFlags & UP) == 0) {
                return false;
            }
            if (dy > 0 && (swipeFlags & DOWN) == 0) {
                return false;
            }
        }
        mDx = mDy = 0f;
        mActivePointerId = motionEvent.getPointerId(0);
        // 表示当前ItemView被侧滑行为选中
        select(vh, ACTION_STATE_SWIPE);
        return true;
    }

  checkSelectForSwipe方法的代码相对来说比较长,但是无非就是判断当前ItemView是否符合侧滑行为,如果到最后符合的话,那么就会调用select方法来初始化一些值。   同时,我们看一下checkSelectForSwipe方法的调用时机只有两个地方:

  1. onTouchEvent方法
  2. onInterceptTouchEvent方法

  调用的时机也是比较正确的,至于为什么需要两个地方来调用这个方法,我也不太清楚,估计做什么保险操作吧。

B. 拖动选中

  拖动选中的时机比较简单,因为拖动触发的前提是长按ItemView,所以我们直接在ItemTouchHelperGestureListeneronLongPress方法找到相关代码:

        @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)) {
                        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);
                        }
                    }
                }
            }
        }

  这段代码表达的意思非常简单,这里我就不多余的解释了。从这里可以看出来,最终还是调用了select方法表示选中一个ItemView

(3). ItemView随着手指滑动

  我们知道了ItemTouchHelper怎么进行手势判断来选中一个ItemView,选中之后的操作就是ItemView随着手指滑动,我们来看看ItemView是怎么实现的。

  我们知道,随着手指的滑动,onTouchEvent方法会被调用,我们来看看相关的代码:

        public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
            // ······
            switch (action) {
                case MotionEvent.ACTION_MOVE: {
                    // Find the index of the active pointer and fetch its position
                    if (activePointerIndex >= 0) {
                        updateDxDy(event, mSelectedFlags, activePointerIndex);
                        moveIfNecessary(viewHolder);
                        mRecyclerView.removeCallbacks(mScrollRunnable);
                        mScrollRunnable.run();
                        mRecyclerView.invalidate();
                    }
                    break;
                }
                // ······
            }
        }

  上面的代码我将它分为4步:

  1. 更新mDxmDy的值。mDxmDy表示手指在x轴和y轴上分别滑动的距离。
  2. 如果需要,移动其他ItemView的位置。这个主要针对拖动行为。
  3. 如果需要,滑动RecyclerView。这个主要针对拖动行为,而这里滑动RecyclerView的条件就是,RecyclerView本身有大量的数据,一屏显示不完,此时如果拖动一个ItemView达到RecyclerView的底部或者顶部,会滑动RecyclerView
  4. 更新被选中的ItemView的位置。代码体现在mRecyclerView.invalidate()

  其中,更新mDxmDy的值是通过updateDxDy方法来实现的,而updateDxDy方法方法比较简单,这里就不展开了。

  我们再来看看第二步,移动其他ItemView的位置主要是通过moveIfNecessary方法实现的。我们来看看具体的代码:

    void moveIfNecessary(ViewHolder viewHolder) {
        // ······
        // 以上都是不符合move的条件
        // 1.寻找可能会交换位置的ItemView
        List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
        if (swapTargets.size() == 0) {
            return;
        }
        // 2.找到符合条件交换的ItemView
        // may swap.
        ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);
        if (target == null) {
            mSwapTargets.clear();
            mDistances.clear();
            return;
        }
        final int toPosition = target.getAdapterPosition();
        final int fromPosition = viewHolder.getAdapterPosition();
        // 3.回调Callback里面的onMove方法,这个方法需要我们手动实现
        if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
            // 保证target的可见
            // keep target visible
            mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
                    target, toPosition, x, y);
        }
    }

  如上就是moveIfNecessary方法的代码,这里讲它分为3步:

  1. 调用findSwapTarget方法,寻找可能会跟选中的ItemView交换位置的ItemView。这里判断的条件是只要选中的ItemView跟某一个ItemView重叠,那么这个ItemView可能会跟选中的ItemView交换位置。
  2. 调用Callback的chooseDropTarget方法来找到符合交换条件的ItemView。这里符合的条件是指,选中的ItemViewbottom大于目标ItemViewbottom或者ItemViewtop大于目标ItemViewtop。通常来说,我们可以重写chooseDropTarget方法,来定义什么条件下就交换位置。
  3. 回调CallbackonMove方法,这个方法需要我们自己实现。这里需要注意的是,如果onMove方法返回为true的话,会调用Callback另一个onMove方法来保证target可见。为什么必须保证target可见呢?从官方文档上来看的话,如果target不可见,在某些滑动的情形下,target会被remove掉(回收掉),从而导致drag过早的停止。

  关于ItemTouchHelper是怎么来选择交换位置的ItemView,重点就在findSwapTarget方法和chooseDropTarget方法。其中findSwapTarget方法是找到可能会交换位置的ItemViewchooseDropTarget方法是找到会交换位置的ItemView,这是两个方法的不同点。同时,如果此时在拖动,但是拖动的ItemView还未达到交换条件,也就是跟另一个ItemView只是重叠了一小部分,这种情况下,findSwapTargets方法返回的集合不为空,但是chooseDropTarget方法寻找的ItemView为空。

  然后就是第三步,第三步的作用是当ItemView拖动到边缘,如果此时RecyclerView可以滑动,那么RecyclerView会滚动。具体的实现是在mScrollRunnablerun方法调用:

    final Runnable mScrollRunnable = new Runnable() {
        @Override
        public void run() {
            if (mSelected != null && scrollIfNecessary()) {
                if (mSelected != null) { //it might be lost during scrolling
                    moveIfNecessary(mSelected);
                }
                mRecyclerView.removeCallbacks(mScrollRunnable);
                ViewCompat.postOnAnimation(mRecyclerView, this);
            }
        }
    };

  在run方法里面通过scrollIfNecessary方法来判断RecyclerView是否滚动,如果需要滚动,scrollIfNecessary方法会自动完成滚动操作。

  最后一步就是ItemView位置的更新,也就是mRecyclerView.invalidate()的执行。这里需要理解的是,为什么通过invalidate方法就能更新ItemView的位置呢?因为ItemView在随着手指移动时,变化的是translationXtranslationY两个属性,所以只需要调用invalidate方法就行。调用invalidate方法之后,相当于RecyclerView会重新绘制一次,那么所有ItemDecorationonDrawonDrawOver方法都会被调用,而恰好的是,ItemTouchHelper就是一个ItemDecoration。我们想要知道ItemView是怎么随着手指移动的,答案就在onDraw方法里面:

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        // ······
        mCallback.onDraw(c, parent, mSelected,
                mRecoverAnimations, mActionState, dx, dy);
    }

  在onDraw方法里面,调用了CallbackonDraw方法。我们来看看CallbackonDraw方法:

        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);
            }
        }

  代码还是比较长,但是表示的意思是非常简单的。就是调用onChildDraw方法,将所有正在交换位置的ItemView和被选中的ItemView作为参数传递过去。

  而在onChildDraw方法里面,调用了ItemTouchUIUtilonDraw方法。我们从ItemTouchUiUtil的实现类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);
        }

  在这里改变了每个ItemViewtranslationXtranslationY,从而实现了ItemView随着手指移动的效果。

  从这里,我们可以看出来,一旦调用RecyclerViewinvalidate方法,ItemTouchHelperonDraw方法和onDrawOver方法都会被执行。这个可能就是ItemTouchHelper继承ItemDecoration的原因吧。

(4).为什么拖动的ItemView始终在其他ItemView的上面?

  当我们在上下拖动的时候,我们发现一个问题,就是拖动的ItemView始终在其他ItemView的上面。这里,我们不禁疑惑,我们都知道,在ViewGroup里面,所有的child都有绘制顺序。通常来说,先添加的child先绘制,后添加的child后绘制,在RecyclerView中也是不例外,上面的ItemView先绘制,而下面的ItemView后绘制。而在这个拖动效果中,为什么不符合这个规则呢?我们来看看ItemTouchHelper是怎么帮忙实现的。

  答案得分为两个种情况,一种是Api小于21,一种是Api大于等于21。

  我们先来看看Api小于21的情况。这个得从addChildDrawingOrderCallback方法里面去寻找答案:

    private void addChildDrawingOrderCallback() {
        if (Build.VERSION.SDK_INT >= 21) {
            return; // we use elevation on Lollipop
        }
        if (mChildDrawingOrderCallback == null) {
            mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
                @Override
                public int onGetChildDrawingOrder(int childCount, int i) {
                    if (mOverdrawChild == null) {
                        return i;
                    }
                    int childPosition = mOverdrawChildPosition;
                    if (childPosition == -1) {
                        childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
                        mOverdrawChildPosition = childPosition;
                    }
                    if (i == childCount - 1) {
                        return childPosition;
                    }
                    return i < childPosition ? i : i + 1;
                }
            };
        }
        mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback);
    }

  实现的原理就是给RecyclerView设置了一个ChildDrawingOrderCallback接口来改变child的绘制顺序,这样能保证被选中的ItemView后于重叠的ItemView绘制,这样就实现了被选中的ItemView始终在上面。

  不过使用ChildDrawingOrderCallback接口时,我们需要注意的是:要想是接口有效,必须保证所有childelevation是一样的,如果不一样,那么elevation优先级更高

  从上面的注意点,我们应该都知道Api大于等于21时,使用的是什么方式来实现的吧。没错就是通过改变 ItemViewelevation值实现的。我们来看看具体实现,在Api21ImplonDraw方法里面:

        @Override
        public void onDraw(Canvas c, RecyclerView recyclerView, View view,
                float dX, float dY, int actionState, boolean isCurrentlyActive) {
            if (isCurrentlyActive) {
                Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
                if (originalElevation == null) {
                    originalElevation = ViewCompat.getElevation(view);
                    float newElevation = 1f + findMaxElevation(recyclerView, view);
                    ViewCompat.setElevation(view, newElevation);
                    view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
                }
            }
            super.onDraw(c, recyclerView, view, dX, dY, actionState, isCurrentlyActive);
        }

  因为这里使用的是ViewCompcat,所以当Api小于21时,调用setElevation是无效的。如上就是Api大于等于21时实现被选中的ItemView在所有ItemView上面的代码。

(5). 手势释放之后

  不管是拖动还是侧滑,当我们手势释放之后,做的操作无非两种:1. 回到原位;2.移动到正确的位置。那这部分的具体实现在哪里呢?没错,就在我们之前分析过的select方法里面,此时看select方法代码时,我们需得注意两个点:

  1. 此时,参数selected为null。
  2. 此时,变量mSelected不为null。

  然后,我们在来看看相关代码:

    void select(ViewHolder selected, int actionState) {
        // ······
        if (mSelected != null) {
            final ViewHolder prevSelected = mSelected;
            if (prevSelected.itemView.getParent() != null) {
                // 1. 计算需要移动的距离
                final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0
                        : swipeIfNecessary(prevSelected);
                releaseVelocityTracker();
                // find where we should animate to
                final float targetTranslateX, targetTranslateY;
                int animationType;
                switch (swipeDir) {
                    case LEFT:
                    case RIGHT:
                    case START:
                    case END:
                        targetTranslateY = 0;
                        targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
                        break;
                    case UP:
                    case DOWN:
                        targetTranslateX = 0;
                        targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight();
                        break;
                    default:
                        targetTranslateX = 0;
                        targetTranslateY = 0;
                }
                if (prevActionState == ACTION_STATE_DRAG) {
                    animationType = ANIMATION_TYPE_DRAG;
                } else if (swipeDir > 0) {
                    animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
                } else {
                    animationType = ANIMATION_TYPE_SWIPE_CANCEL;
                }
                getSelectedDxDy(mTmpPosition);
                final float currentTranslateX = mTmpPosition[0];
                final float currentTranslateY = mTmpPosition[1];
                // 2.创建动画
                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;
                        }
                        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);
                        }
                    }
                };
                final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
                        targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
                rv.setDuration(duration);
                mRecoverAnimations.add(rv);
                // 3.执行动画
                rv.start();
                preventLayout = true;
            } else {
                removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
                mCallback.clearView(mRecyclerView, prevSelected);
            }
            mSelected = null;
        }
        // ······
    }

  上面的代码还是比较长,我简单的将它分为3步,分别是:

  1. 计算ItemView此时需要移动的距离。
  2. 根据计算出来的距离,创建动画。
  3. 执行动画,让ItemView回到正确的位置。

  而这三步的具体实现都是比较简单的,在这里就不过多的解释了。

4.总结

  到此为止,ItemTouchHelper就差不多了,在这里我对ItemTouchHelper做一个简单的总结。

  1. 我们使用ItemTouchHelper时,需要实现一个ItemTouchHelper.Callback类。在这个实现类里面,我们需要实现 三个方法,分别是:1. getMovementFlags,主要是设置ItemTouchHelper执行那些行为和方向;2. onMove方法,表示当前有两个ItemView发生了交换,此时需要我们更新数据源;3. onSwiped方法,表示当前有ItemView被侧滑删除,也需要我们更新数据源。
  2. ItemTouochHelper是通过ItemTouchListener来获取每个ItemView的事件,通过GestureDetector来判断长按行为。
  3. ItemTouchHelper是通过改变ItemViewtranslationXtranslationY属性值,进而改变每个ItemView的位置。
  4. ItemTouchHelper是通过ChildDrawingOrderCallback接口和Elevation来改变ItemView的绘制顺序的。