RecyclerView高级进阶总结:ItemTouchHelper实现拖拽和侧滑删除

时间:2024-04-05 10:27:25

前言

现在RecyclerView的应用越来越广泛了,不同的应用场景需要其作出不同的改变。有时候我们可能需要实现侧滑删除的功能,又或者长按Item进行拖动与其他Item进行位置的交换,但RecyclerView没有提供现成的API供我们操作,但是SDK提供了ItemTouchHelper这样一个工具类帮助我们快速实现以上功能。RecyclerView具体使用在这里不详细说明了,详细了解的话请查看详解RecyclerView替换ListView和GridView及实现暴瀑流,接下来我们主要来介绍一下ItemTouchHelper。


ItemTouchHelper是什么?

查看Android源码API:Android-26ItemTouchHelper是如下这样定义的:

RecyclerView高级进阶总结:ItemTouchHelper实现拖拽和侧滑删除

大概意思:这是一个工具类,可实现滑动删除和拖拽移动,使用这个工具类需要RecyclerView和Callback,它配置了启用了何种类型的交互,并且当用户执行这些操作时也会接收事件。
根据您支持的功能,您应该重写方法onMove()(回收站、ViewHolder、ViewHolder)和/或重写onSwiped()方法(ViewHolder,int)....。

默认情况下,ItemTouchHelper移动条目的translatex/y属性来重新定位它们。您可以通过覆盖onChilDraw()回调来定制这些行为(Canvas、回收器视图、ViewHolder、float、float、int、boolean)或onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)大多数情况下,你只需要重写onChildDraw。


————————————————————————————————————————————————————————


通过上面对ItemTouchHelper的定义分析我们知道,要想实现对RecyclerView item的侧滑删除,代码如下:



定义一个类IHCallback继承ItemTouchHelper.Callback,重写相关方法,代码如下:


public class IHCallback extends ItemTouchHelper.Callback {

    private MoveAdapter moveAdapter;

    public IHCallback(MoveAdapter moveAdapter) {
        this.moveAdapter = moveAdapter;
    }

    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        //允许上下的拖动
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        //只允许从右向左侧滑
        int swipeFlags = ItemTouchHelper.LEFT;
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        moveAdapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {

        moveAdapter.onItemDissmiss(viewHolder.getAdapterPosition());
    }

    @Override
    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);
        if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
            //滑动时改变Item的透明度
            final float alpha = 1 - Math.abs(dX) / (float)viewHolder.itemView.getWidth();
            viewHolder.itemView.setAlpha(alpha);
        }
    }
}

上面标记红色的代码表示重写的侧滑删的方法。onMove方法主要是移动Item(eg:上下,左右移动),ItemToucherHelper相关方法详细用法也可以查看官方文档点击打开链接,接下来我们我们介绍一下ItemToucherHelper基本使用方法。


ItemToucherHelper基本使用:


首先我们要使用RecyclerView肯定回用到自定义适配器adapter,那么问题来了,我们要考虑Adapter和ItemTouchHelper之间数据如何进行操作,因为ItemTouchHelper在完成触摸的各种动作后,就要对Adapter的数据进行操作,比如侧滑删除操作,最后需要调用Adapter的notifyItemRemove()方法来移除该数据。因此我们可以把数据操作的部分抽象成一个接口方法,让ItemTouchHelper.Callback重写的方法调用对应方法即可。具体如下:


一.新建接口:IOperationData


public interface IOperationData {
    /**
     * 数据交换
     * @param fromPosition
     * @param toPosition
     */
    void onItemMove(int fromPosition,int toPosition);

    /**
     * 数据删除
     * @param position
     */
    void onItemDissmiss(int position);
}

我们自定义的adapter实现IOperationData这个接口,代码如下:


public class MoveAdapter extends RecyclerView.Adapter<MoveAdapter.MoveHolder> implements IOperationData {

   private List<String> mDataList;
   private LayoutInflater mInflater;
   private Context mContext;

   public MoveAdapter(Context context) {
      this.mContext = context;
      mInflater =LayoutInflater.from(mContext);
   }

   MoveAdapter(List<String> dataList) {
      mDataList = dataList;
   }

   public void setData(List<String> dataList) {
      mDataList = dataList;
      notifyDataSetChanged();
   }

   @Override
   public MoveHolder onCreateViewHolder(ViewGroup parent, int viewType) {

      return new MoveHolder(mInflater.inflate(R.layout.item_move, parent, false));
   }

   @Override
   public void onBindViewHolder(MoveHolder holder, int position) {
      holder.mTextTitle.setText(mDataList.get(position));
   }

   @Override
   public int getItemCount() {
      return mDataList == null ? 0 : mDataList.size();
   }

   @Override
   public void onItemMove(int fromPosition, int toPosition) {
      //交换位置
      Collections.swap(mDataList,fromPosition,toPosition);
      notifyItemMoved(fromPosition,toPosition);

   }

   @Override
   public void onItemDissmiss(int position) {
      //移除数据
      mDataList.remove(position);
      notifyItemRemoved(position);
   }

   static class MoveHolder extends RecyclerView.ViewHolder {

      TextView mTextTitle;

      MoveHolder(View itemView) {
         super(itemView);
         mTextTitle = itemView.findViewById(R.id.tv_move);
      }
   }

}

在xxCallback extends ItemTouchHelper.Callback 对应的方法调用我们接口里刚才申明的方法。



二.创建类IHCallback继承ItemTouchHelper.Callback

ItemTouchHelper的定义我们知道,使用ItemTouchHelper需要一个Callback,该Callback是ItemTouchHelper.Callback的子类,所以我们需要新建一个类比如

IHCallback继承自ItemTouchHelper.Callback。我们可以重写其数个方法来实现我们的需求。代码如下:


public class IHCallback extends ItemTouchHelper.Callback {

    private MoveAdapter moveAdapter;

    public IHCallback(MoveAdapter moveAdapter) {
        this.moveAdapter = moveAdapter;
    }

    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        //允许上下的拖动
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        //只允许从右向左侧滑
        int swipeFlags = ItemTouchHelper.LEFT;
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        moveAdapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {

        moveAdapter.onItemDissmiss(viewHolder.getAdapterPosition());//调用我们自定义的方法
    }

    @Override
    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);
        if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
            //滑动时改变Item的透明度
            final float alpha = 1 - Math.abs(dX) / (float)viewHolder.itemView.getWidth();
            viewHolder.itemView.setAlpha(alpha);
        }
    }
}


接下来我们从源码的角度具体分析每个方法的作用,代码如下:


/**
 * Should return a composite flag which defines the enabled move directions in each state
 * (idle, swiping, dragging).
 * <p>
 * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int,
 * int)}
 * or {@link #makeFlag(int, int)}.
 * <p>
 * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next
 * 8 bits are for SWIPE state and third 8 bits are for DRAG state.
 * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in
 * {@link ItemTouchHelper}.
        这个标志由3组8位组成,其中前8位为空闲状态,下一个
        8位是用于滑动状态的,第三位是用于拖动状态的。
      每个8位的部分都可以通过简单的或“ing的方向标志”来构造。
* <p> * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to * swipe by swiping RIGHT, you can return: * <pre> * makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT); * </pre> * This means, allow right movement while IDLE and allow right and left movement while * swiping. * * @param recyclerView The RecyclerView to which ItemTouchHelper is attached. * @param viewHolder The ViewHolder for which the movement information is necessary. * @return flags specifying which movements are allowed on this ViewHolder. * @see #makeMovementFlags(int, int) * @see #makeFlag(int, int) */ public abstract int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder);

此方法,从命名上就可以知道它是关于移动的,应该返回一个复合标志,它定义了每个状态下启用的移动方向,比如说允许从右到左侧滑,允许上下拖动等,具体我们由方法makeMovementFlags实现,代码如下:


/**
 * Convenience method to create movement flags.
 * <p>
 * For instance, if you want to let your items be drag & dropped vertically and swiped
 * left to be dismissed, you can call this method with:
 * <code>makeMovementFlags(UP | DOWN, LEFT);</code>
 *
 * @param dragFlags  The directions in which the item can be dragged.
 * @param swipeFlags The directions in which the item can be swiped.
 * @return Returns an integer composed of the given drag and swipe flags.
 */
public static int makeMovementFlags(int dragFlags, int swipeFlags) {
    return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags)
            | makeFlag(ACTION_STATE_SWIPE, swipeFlags)
            | makeFlag(ACTION_STATE_DRAG, dragFlags);
}


/**
 * Shifts the given direction flags to the offset of the given action state.
 *
 * @param actionState The action state you want to get flags in. Should be one of
 *                    {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or
 *                    {@link #ACTION_STATE_DRAG}.
 * @param directions  The direction flags. Can be composed from {@link #UP}, {@link #DOWN},
 *                    {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}.
 * @return And integer that represents the given directions in the provided actionState.
 */
public static int makeFlag(int actionState, int directions) {
    return directions << (actionState * DIRECTION_FLAG_COUNT);
}



  actionState 说明
  ACTION_STATE_IDLE 空闲状态
  ACTION_STATE_SWIPE 滑动
  ACTION_STATE_DRAG 拖动
针对swipe和drag状态,设置不同状态(swipe、drag)下支持的方向 (LEFT, RIGHT, START, END, UP, DOWN)
 idle:0-7位表示swipe和drag的方向
 swipe:8-15位表示滑动方向
 drag:16-23位表示拖动方向

__________________________________________________________________________________________________________
接下来我们在看看ItemTouchHelper.Callback的其他方法:



/**
 * Return true if the current ViewHolder can be dropped over the the target ViewHolder.
 换句话说我们一般用drag来做一些换位置的操作,就是当前target对应的item是否可以换位置
 * <p>
 * This method is used when selecting drop target for the dragged View. After Views are
 * eliminated either via bounds check or via this method, resulting set of views will be
 * passed to {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)}.
 * <p>
 * Default implementation returns true.
 *
 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to.
 * @param current      The ViewHolder that user is dragging.
 * @param target       The ViewHolder which is below the dragged ViewHolder.
 * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false
 * otherwise.
 */
public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
        ViewHolder target) {
    return true;
}

/**
 * Returns whether ItemTouchHelper should start a drag and drop operation if an item is
 * long pressed.
 * <p>
 * Default value returns true but you may want to disable this if you want to start
 * dragging on a custom view touch using {@link #startDrag(ViewHolder)}.
 * 该方法返回true时,表示支持长按拖动,即长按ItemView后才可以拖动,我们遇到的场景一般也是这样的
 * 默认是返回true
 * @return True if ItemTouchHelper should start dragging an item when it is long pressed,
 * false otherwise. Default value is <code>true</code>.
 * @see #startDrag(ViewHolder)
 */
public boolean isLongPressDragEnabled() {
    return true;
}
/**
 * Called when ItemTouchHelper wants to move the dragged item from its old position to
 * the new position.
 * <p>
 * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved
 * to the adapter position of {@code target} ViewHolder
 * ({@link ViewHolder#getAdapterPosition()
 * ViewHolder#getAdapterPosition()}).
 * <p>
 * If you don't support drag & drop, this method will never be called.
 *
 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to.
 * @param viewHolder   The ViewHolder which is being dragged by the user.
 * @param target       The ViewHolder over which the currently active item is being
 *                     dragged.
 * @return True if the {@code viewHolder} has been moved to the adapter position of
 * {@code target}.
 * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int)
 */
public abstract boolean onMove(RecyclerView recyclerView,
        ViewHolder viewHolder, ViewHolder target);

当用户拖动一个Item进行上下移动从旧的位置到新的位置的时候会调用该方法,在该方法内,
我们可以调用Adapter的notifyItemMoved方法来交换两个ViewHolder的位置,最后返回true,
表示被拖动的ViewHolder已经移动到了目的位置。所以,如果要实现拖动交换位置,可以重写该方法(前提是支持上下拖动)


/**
 * Called when a ViewHolder is swiped by the user.
 * <p>
 * If you are returning relative directions ({@link #START} , {@link #END}) from the
 * {@link #getMovementFlags(RecyclerView, ViewHolder)} method, this method
 * will also use relative directions. Otherwise, it will use absolute directions.
 * <p>
 * If you don't support swiping, this method will never be called.
 * <p>
 * ItemTouchHelper will keep a reference to the View until it is detached from
 * RecyclerView.
 * As soon as it is detached, ItemTouchHelper will call
 * {@link #clearView(RecyclerView, ViewHolder)}.
 *
 * @param viewHolder The ViewHolder which has been swiped by the user.
 * @param direction  The direction to which the ViewHolder is swiped. It is one of
 *                   {@link #UP}, {@link #DOWN},
 *                   {@link #LEFT} or {@link #RIGHT}. If your
 *                   {@link #getMovementFlags(RecyclerView, ViewHolder)}
 *                   method
 *                   returned relative flags instead of {@link #LEFT} / {@link #RIGHT};
 *                   `direction` will be relative as well. ({@link #START} or {@link
 *                   #END}).
 */
public abstract void onSwiped(ViewHolder viewHolder, int direction);
当用户左右滑动Item达到删除条件时,会调用该方法,一般手指触摸滑动的距离达到RecyclerView宽度的一半时,再松开手指,此时该Item会继续向原先滑动方向滑过去并且调用onSwiped方法进行删除,否则会反向滑回原来的位置。


/**
 * Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped
 * over the View.
 * <p>
 *   
 * Default value returns true but you may want to disable this if you want to start
 * swiping on a custom view touch using {@link #startSwipe(ViewHolder)}.
 * 该方法返回true时,表示如果用户触摸滑动了item,那么可以执行滑动操作,
 * 即可以调用到startSwipe()方法
 *  默认是返回true。
 * @return True if ItemTouchHelper should start swiping an item when user swipes a pointer
 * over the View, false otherwise. Default value is <code>true</code>.
 * @see #startSwipe(ViewHolder)
 */
public boolean isItemViewSwipeEnabled() {
    return true;
}

/**
 * 当ViewHolder通过ItemTouchHelper滑动或拖动时被调用。
 * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed.
 * <p/>
 * If you override this method, you should call super.
 *
 * @param viewHolder  The new ViewHolder that is being swiped or dragged. Might be null if
 *                    it is cleared.
 * @param actionState One of {@link ItemTouchHelper#ACTION_STATE_IDLE},
 *                    {@link ItemTouchHelper#ACTION_STATE_SWIPE} or
 *                    {@link ItemTouchHelper#ACTION_STATE_DRAG}.
 * @see #clearView(RecyclerView, RecyclerView.ViewHolder)
 */
public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
    if (viewHolder != null) {
        sUICallback.onSelected(viewHolder.itemView);
    }
}

/**
 * 当用户与一个元素的交互结束时,由ItemTouchHelper调用
 也完成了动画
 * Called by the ItemTouchHelper when the user interaction with an element is over and it
 * also completed its animation.
 * <p>
 * This is a good place to clear all changes on the View that was done in
 * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)},
 * {@link #onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int,
 * boolean)} or
 * {@link #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)}.
 *
 * @param recyclerView The RecyclerView which is controlled by the ItemTouchHelper.
 * @param viewHolder   The View that was interacted by the user.
 */
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
    sUICallback.clearView(viewHolder.itemView);
}
一般我们在该方法内恢复ItemView的初始状态,防止由于复用而产生的显示错乱问题。

/**
 * Called by ItemTouchHelper on RecyclerView's onDraw callback.
 * <p>
 * 如果你想要定制你的视图对用户交互的响应,这是
 这是一个可以重写的好方法。
 * If you would like to customize how your View's respond to user interactions, this is
 * a good place to override.
 * <p>
 * Default implementation translates the child by the given <code>dX</code>,
 * <code>dY</code>.
 * ItemTouchHelper also takes care of drawing the child after other children if it is being
 * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this
 * is
 * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L
 * and after, it changes View's elevation value to be greater than all other children.)
 *
 * @param c                 The canvas which RecyclerView is drawing its children
 * @param recyclerView      The RecyclerView to which ItemTouchHelper is attached to
 * @param viewHolder        The ViewHolder which is being interacted by the User or it was
 *                          interacted and simply animating to its original position
 * @param dX                The amount of horizontal displacement caused by user's action
 * @param dY                The amount of vertical displacement caused by user's action
 * @param actionState       The type of interaction on the View. Is either {@link
 *                          #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}.
 * @param isCurrentlyActive True if this view is currently being controlled by the user or
 *                          false it is simply animating back to its original state.
 * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int,
 * boolean)
 */
public void onChildDraw(Canvas c, RecyclerView recyclerView,
        ViewHolder viewHolder,
        float dX, float dY, int actionState, boolean isCurrentlyActive) {
    sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
            isCurrentlyActive);
}
我们可以在这个方法内实现我们自定义的交互规则或者自定义的动画效果,比如,滑动删除变透明等。

代码实现效果,如下:

RecyclerView高级进阶总结:ItemTouchHelper实现拖拽和侧滑删除


源码下载https://github.com/yangxiansheng123/RecyItemTouchHelper