实现拖拽和侧滑之前我门需要先了解一个recyclerview的辅助类ItemTouchHelper

ItemTouchHelper是一个工具类,可实现侧滑删除和拖拽移动,使用这个工具类需要RecyclerView和Callback。同时根据需要重写onMove和onSwiped方法。接下来就来讲述ItemTouchHelper的使用方法。

ItemTouchHelper 常用的函数列出如下:

/**
     * 针对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 实现一些基本的业务操作 数据交换等等

public class SwapCallBack extends ItemTouchHelper.Callback {
    private List<ImgBean> datas;
    private RecyclerView.Adapter adapter;
    private Context mContext;

    public SwapCallBack(List<ImgBean> datas, RecyclerView.Adapter adapter, Context mContext) {
        this.datas = datas;
        this.adapter = adapter;
        this.mContext = mContext;
    }

    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        int dragFrlg = 0;
        if (recyclerView.getLayoutManager() instanceof GridLayoutManager){
            dragFrlg = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
        }else {
            dragFrlg = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        }
        return makeMovementFlags(dragFrlg,0);
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        //滑动事件 下面注释的代码,滑动后数据和条目错乱,被舍弃
//        Collections.swap(datas,viewHolder.getAdapterPosition(),target.getAdapterPosition());
//        adapter.notifyItemMoved(viewHolder.getAdapterPosition(),target.getAdapterPosition());
        //得到当前拖拽的viewHolder的position
        int fromPosition = viewHolder.getAdapterPosition();
        //得到当前要拖拽到的item的viewHolder
        int toPosition = target.getAdapterPosition();
        if (fromPosition < datas.size() && toPosition < datas.size()){
            if (fromPosition < toPosition){
                for (int i = fromPosition; i < toPosition; i++) {
                    Collections.swap(datas , i ,i + 1);
                }
            }else {
                for (int i = fromPosition; i > toPosition; i--) {
                    Collections.swap(datas , i ,i - 1);
                }
            }
            adapter.notifyItemMoved(fromPosition,toPosition);
        }
        return true;
    }

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
        //侧面滑动使用
    }

    @Override
    public boolean isLongPressDragEnabled() {
        return true;
    }

    /**
     * 长按选中Item的时候开始调用
     * 长按高亮
     * @param viewHolder
     * @param actionState
     */
    @Override
    public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE){
//            viewHolder.itemView.setBackgroundColor(Color.RED);
            Vibrator vib = (Vibrator) mContext.getSystemService(Service.VIBRATOR_SERVICE);
            vib.vibrate(50);
        }
        super.onSelectedChanged(viewHolder, actionState);
    }

    /**
     * 手指松开时还原高亮
     * @param recyclerView
     * @param viewHolder
     */
    @Override
    public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
//        viewHolder.itemView.setBackgroundColor(0);
        adapter.notifyDataSetChanged();
    }
}

然后 将SwapCallBack对象传给ItemTouchHelper 去处理

ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwapCallBack(list,otherAdapter,this));

然后将ItemTouchHelper 绑定一个我们要操作reyclerView

itemTouchHelper.attachToRecyclerView(recyclerView);

实现效果如下:

android recyclerview item内容显示不全 recyclerview itemtouchhelper_android studio

3.自定义侧滑动画
有时候我们对默认的动画效果可能不满意,需要自己实现想要的动画效果,ItemTouchHelper.Callback提供的onChildDraw方法可以让我们很方便地实现想要的效果。

该效果是比较常见的,用户向左滑动Item的时候,一开始提示的是“左滑删除”,滑动到一定距离后,显示删除的图标,并且随着滑动距离的增加该图标不断变大,达到最大后用户松开手指,该Item被删除。
接下来我们来分析一下怎样实现以上的效果:
首先,要想左滑出现一个删除的方块,可以在LinearLayout放一个这样的“方块”,让它与Item水平并排排列,以下是布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#fff"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <com.haocai.itemtouchhelper.view.CircleImageView
        android:id="@+id/iv_logo"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="8dp"
        android:src="@drawable/logo1" />

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dip"
        android:orientation="vertical"
        android:paddingRight="5dp">

        <RelativeLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical">

            <TextView
                android:id="@+id/tv_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:text="小王"
                android:textColor="#000"
                android:textSize="16sp" />

            <TextView
                android:id="@+id/tv_time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:text="14:31"
                android:textColor="#a6a6a6"
                android:textSize="12sp" />
        </RelativeLayout>

        <RelativeLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dip"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tv_lastMsg"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:layout_centerVertical="true"
                android:text="一起吃饭"
                android:textColor="#808080"
                android:textSize="12sp" />

            <ImageView
                android:id="@+id/iv_pop"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true" />
        </RelativeLayout>
    </LinearLayout>

    <FrameLayout
        android:layout_width="100dp"
        android:layout_height="match_parent"
        android:background="#f33213">

        <ImageView
            android:id="@+id/iv_detele"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_gravity="center"
            android:src="@drawable/delete"
            android:visibility="invisible" />

        <TextView
            android:id="@+id/tv_detele"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="左滑删除"
            android:textColor="#ffffff"
            android:textSize="18sp" />
    </FrameLayout>
</LinearLayout>

布局文件修改后,我们尝试来滑动一下,发现后面的删除方块并不会出现,这是因为默认的滑动方式是setTranslationX(int),即是对整个View的滑动,所以无论我们怎样滑动,都不会出现删除方块。因此,我们要改变一个种滑动方式,比如使用scrollTo(int,int),这种是对View的内容的滑动,所以随着左滑,item会向左滑去,而位于右方的方块自然也就出现了。
接着,我们考虑该“删除眼睛”的图标是怎样从小变大的,这个实现也比较简单,只要根据滑动的距离对该ImageView的LayoutParams.width进行改变就行了,不过要注意限制大小,否则过大会造成图片的失真。当滑动距离等于RecyclerView宽度的一半时,此时松开手会使Item删除,那么我们可以在该滑动距离达到该值时时“眼睛”变得最大,此时可以达到良好的交互效果,提示了用户无需继续滑动即可删除该Item了。
最后我们要考虑的是:在删除了Item或者没删除而滑回原来的位置后,我们要把所做的改变重置一下,否则,会由于RecyclerView的复用而导致其他位置的ViewHolder与当前的ViewHolder所做的改变一样,即造成显示的错误。我们可以在clearView()方法内重置改变,这样就能解决因复用而导致的显示问题了。
最后我们来看看SimpleItemTouchHelperCallback的代码:

public class MyItemTouchHelperCallback3 extends ItemTouchHelper.Callback {

    //限制ImageView长度所能增加的最大值
    private double ICON_MAX_SIZE = 40;
    //ImageView的初始长宽
    private int fixedWidth = 120;

    private ItemTouchMoveListener moveListener;

    public MyItemTouchHelperCallback3(ItemTouchMoveListener moveListener) {
        this.moveListener = moveListener;
    }

//    /**
//     * 设置滑动类型标记
//     *
//     * @param recyclerView
//     * @param viewHolder
//     * @return
//     *          返回一个整数类型的标识,用于判断Item那种移动行为是允许的
//     */
//    @Override
//    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//        //START  右向左 END左向右 LEFT  向左 RIGHT向右  UP向上
//        //如果某个值传0,表示不触发该操作
//        return makeMovementFlags(ItemTouchHelper.UP|ItemTouchHelper.DOWN,ItemTouchHelper.END );
//    }

    /**
     * Callback回调监听时先调用的,用来判断当前是什么动作,比如判断方向
     * 作用:哪个方向的拖动
     *
     * @param recyclerView
     * @param viewHolder
     * @return
     */
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        //方向:up,down,left,right
        //常量
        // ItemTouchHelper.UP    0x0001
        // ItemTouchHelper.DOWN  0x0010
        // ItemTouchHelper.LEFT
        // ItemTouchHelper.RIGHT

        //我要监听的拖拽方向是哪个方向
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        //我要监听的swipe侧滑方向是哪个方向
        int swipeFlags = ItemTouchHelper.LEFT ;


        int flags = makeMovementFlags(dragFlags, swipeFlags);
        return flags;
    }


    /**
     * 是否打开长按拖拽效果
     *
     * @return
     */
    @Override
    public boolean isLongPressDragEnabled() {
        return true;
    }
    /**
     * Item是否支持滑动
     *
     * @return
     *          true  支持滑动操作
     *          false 不支持滑动操作
     */
    @Override
    public boolean isItemViewSwipeEnabled() {
        return true;
    }
    //当上下移动的时候回调的方法
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder srcHolder, RecyclerView.ViewHolder targetHolder) {
        // 在拖拽过程中不断地调用adapter.notifyItemMoved(from,to);
        if (srcHolder.getItemViewType() != targetHolder.getItemViewType()) {
            return false;
        }
        //在拖拽的过程中不断调用adapter.notifyItemMoved(from,to);
        boolean result = moveListener.onItemMove(srcHolder.getAdapterPosition(), targetHolder.getAdapterPosition());
        return result;
    }

    //侧滑的时候回调的方法
    @Override
    public void onSwiped(RecyclerView.ViewHolder holder, int direction) {
        //监听侧滑,1.删除数据 2.调用adapter.notifyItemRemove(position);
        moveListener.onItemRemove(holder.getAdapterPosition());

    }

    //设置滑动item的背景
    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        //判断选中状态
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
            viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext().getResources().getColor(R.color.colorC));
        }
        super.onSelectedChanged(viewHolder, actionState);

    }

    //清除滑动item的背景
    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        // 恢复
        viewHolder.itemView.setBackgroundColor(Color.WHITE);

        //防止出现复用问题 而导致条目不显示 方式一
        viewHolder.itemView.setAlpha(1);//1-0
        //设置滑出大小
//            viewHolder.itemView.setScaleX(1);
//            viewHolder.itemView.setScaleY(1);

        QQAdapter.MyViewHolder myViewHolder = (QQAdapter.MyViewHolder)viewHolder;
        //重置改变,防止由于复用而导致的显示问题
        viewHolder.itemView.setScrollX(0);
        myViewHolder.tvDetele.setText("左滑删除");
        FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) myViewHolder.ivDetele.getLayoutParams();
        params.width = 150;
        params.height = 150;
        myViewHolder.ivDetele.setLayoutParams(params);
        myViewHolder.ivDetele.setVisibility(View.INVISIBLE);

        super.clearView(recyclerView, viewHolder);
    }

    //设置滑动时item的背景透明度
    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
        QQAdapter.MyViewHolder myViewHolder = (QQAdapter.MyViewHolder)viewHolder;
        //仅对侧滑状态下的效果做出改变
        if (actionState ==ItemTouchHelper.ACTION_STATE_SWIPE){
            Log.d("http","4444");
            //如果dX小于等于删除方块的宽度,那么我们把该方块滑出来
            if (Math.abs(dX) <= getSlideLimitation(viewHolder)){
                viewHolder.itemView.scrollTo(-(int) dX,0);
            }
            //如果dX还未达到能删除的距离,此时慢慢增加“眼睛”的大小,增加的最大值为ICON_MAX_SIZE
            else if (Math.abs(dX) <= recyclerView.getWidth() / 2){
                double distance = (recyclerView.getWidth() / 2 -getSlideLimitation(viewHolder));
                double factor = ICON_MAX_SIZE / distance;
                double diff =  (Math.abs(dX) - getSlideLimitation(viewHolder)) * factor;
                if (diff >= ICON_MAX_SIZE)
                    diff = ICON_MAX_SIZE;
                myViewHolder.tvDetele.setText("");   //把文字去掉
                myViewHolder.ivDetele.setVisibility(View.VISIBLE);  //显示眼睛
                FrameLayout.LayoutParams params = (FrameLayout.LayoutParams)    myViewHolder.ivDetele.getLayoutParams();
                params.width = (int) (fixedWidth + diff);
                params.height = (int) (fixedWidth + diff);
                myViewHolder.ivDetele.setLayoutParams(params);
            }
        }else {
            //拖拽状态下不做改变,需要调用父类的方法
            super.onChildDraw(c,recyclerView,viewHolder,dX,dY,actionState,isCurrentlyActive);
        }
    }
    /**
     * 获取删除方块的宽度
     */
    public int getSlideLimitation(RecyclerView.ViewHolder viewHolder){
        ViewGroup viewGroup = (ViewGroup) viewHolder.itemView;
        return viewGroup.getChildAt(2).getLayoutParams().width;
    }
}

ItemTouchHelper源码走读

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

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

RecyclerView的触摸事件是怎么在ItemTouchHelper里面进行处理的。
ItemTouchHelper是里面怎么判断进入swipe或者drag状态。
进入swipe或者drag状态之后,选中的item是怎么随着手指移动的。
随手指移动的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()函数里面,

checkSelectForSwipe(action, event, activePointerIndex)的调用里面会先去判断是否支持swipe模式Callback.isItemViewSwipeEnabled(),然后去判断swipe支持的方向是否和滑动的方向是否一致Callback.getAbsoluteMovementFlags()。如果这两个条件都满足会在select(vh, ACTION_STATE_SWIPE)函数里面把当前手指下对应的item设置为mSelected,模式对应设置为ACTION_STATE_SWIPE swipe模式。
MotionEvent.ACTION_MOVE的时候先调用updateDxDy(event, mSelectedFlags, activePointerIndex)更新已经滑动的距离,接着调用moveIfNecessary(viewHolder)去设置是否要move,里面也会去回调Callback里面的chooseDropTarget()、onMoved()的函数。接着调用RecyclerView的invalidate()函数迫使RecyclerView去调用onDraw()函数。
MotionEvent.ACTION_UP的时候就是调用了select(null, ACTION_STATE_IDLE)做一些释放操作。

onRequestDisallowInterceptTouchEvent函数里面

如果子view设置disallow的时候会调用select(null, ACTION_STATE_IDLE)函数,其实也好理解,子view都告诉父view不能处理这个事件饿,所以要做一些释放操作。

GestureDetectorCompat类的使用

OnItemTouchListener的帮助类mOnItemTouchListener里面我们看到到了swipe模式的进入时机(onTouchEvent()函数里面checkSelectForSwipe()函数的调用)。但是没有看到drag模式是怎么进入的呀,别着急,另一个帮助类登场了;GestureDetectorCompat。GestureDetectorCompat里面用到的关键的东西都在ItemTouchHelperGestureListener类里面呢:

private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {

        ItemTouchHelperGestureListener() {
        }

        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            View child = findChildView(e);
            if (child != null) {
                ViewHolder vh = mRecyclerView.getChildViewHolder(child);
                if (vh != null) {
                    if (!mCallback.hasDragFlag(mRecyclerView, vh)) {
                        return;
                    }
                    int pointerId = e.getPointerId(0);
                    // Long press is deferred.
                    // Check w/ active pointer id to avoid selecting after motion
                    // event is canceled.
                    if (pointerId == mActivePointerId) {
                        final int index = e.findPointerIndex(mActivePointerId);
                        final float x = e.getX(index);
                        final float y = e.getY(index);
                        mInitialTouchX = x;
                        mInitialTouchY = y;
                        mDx = mDy = 0f;
                        if (DEBUG) {
                            Log.d(TAG,
                                    "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY);
                        }
                        if (mCallback.isLongPressDragEnabled()) {
                            select(vh, ACTION_STATE_DRAG);
                        }
                    }
                }
            }
        }
    }

咦,就处理了一个onLongPress长按手势事件,里面逻辑就是先得到当前MotionEvent事件对应的ViewHolder,调用Callback的hasDragFlag()函数判断是否允许进入drag模式,在调用Callback的isLongPressDragEnabled()函数判断是否允许长按进入drag模式,最后调用select(vh, ACTION_STATE_DRAG)设置mSelected。之后MotionEvent移动事件的处理就都跑到OnItemTouchListener帮助类里面的onTouchEvent()函数里面去了。

通过上面对OnItemTouchListener和手势帮助类GestureDetectorCompat的分析我们可以知道:

swipe和drag模式进入的时机。swipe模式进入的判断是在OnItemTouchListener帮助类里面onTouchEvent()的函数的checkSelectForSwipe()的调用里面判断是否进入,drag模式进入的判断是在GestureDetectorCompat帮助里ItemTouchHelperGestureListener里面onLongPress()里面判断是否进入。
触摸事件在移动的过程中(信息信息请看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()函数里面具体细节。