ItemTouchHelper是RecyclerView高级篇的重要知识之一,它可以实现Item的拖拽以及侧滑删除。
先来看一下效果,如下:
在了解效果的前提下,我将一步步剖析其实现原理。
(1)基本使用
我们首先需要了解,ItemTouchHelper如何使用,看以下代码
//声明一个Callback
ItemTouchHelper.Callback callback = new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
//返回方向值
return 0;
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
//拖拽处理
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
//滑动处理
}
};
//创建helper对象
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
//关联recyclerView
itemTouchHelper.attachToRecyclerView(recyclerview);
或者
//声明一个Callback
ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(dragDirs, swipeDirs) {
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
//拖拽处理
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
//滑动处理
}
};
//创建helper对象
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
//关联recyclerView
itemTouchHelper.attachToRecyclerView(recyclerview);
attachToRecyclerView
方法理解起来比较简单,就是将RecyclerView与ItemTouchHelper进行关联,唯一不清楚的是,Callback中的三个方法没有任何的业务逻辑,我们必须在这三个方法中处理这三个方法的业务逻辑,如果不处理,那么ItemTouchHelper则无任何效果。
在实现Callback内部三个方法业务逻辑之前,我们需要了解这三个方法分别代表的意义:
- getMovementFlags:移动标志;
- onMove:拖拽的处理
- onSwiped:滑动的处理
(2)getMovementFlags: 获取移动状态
ItemTouchHelper帮助类自带创建移动标志的方法,如下代码:
/**
* 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);
}
dragFlags
表示拖拽方向标志,swipeFlags
表示滑动方向标志,这个标志可以理解为一种状态,那么这两个值传递怎样的参数呢?
//控制拖拽的方向(一般是上下左右)
int dragFlags= ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
//控制快速滑动的方向(一般是左右)
int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
我们看一下以上代码,假设:
dragFlags被赋予上下左右四个方向状态,所以拖拽方向状态为:上、下、左、右
swipeFlags 被赋予左右两个方向状态,所以滑动方向状态为:左、右
将拖拽方向状态dragFlags以及滑动方向状态swipeFlags传递到makeMovementFlags
方法中,接着看一下makeFlag
这个方法:
/**
* 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.
*/
@SuppressWarnings("WeakerAccess")
public static int makeFlag(int actionState, int directions) {
return directions << (actionState * DIRECTION_FLAG_COUNT);
}
这个方法最后的返回值为三种状态:无操作状态、拖拽状态、滑动状态(注意和拖拽方向状态、滑动方向状态区别开)
,最终makeMovementFlags
的返回值为:
return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags)
| makeFlag(ACTION_STATE_SWIPE, swipeFlags)
| makeFlag(ACTION_STATE_DRAG, dragFlags);
将无操作状态、滑动状态、拖拽状态合并为移动状态
。
以上所述的状态优点多,您可能会被绕晕,所以专门为大家绘制了一个思维导图:
如图所示,起初通过上下左右这四个方向状态,一步一步的转变为移动状态。
如果您使用SimpleCallback
内部类,在内部有一个方法如下:
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView,
@NonNull ViewHolder viewHolder) {
return makeMovementFlags(getDragDirs(recyclerView, viewHolder),
getSwipeDirs(recyclerView, viewHolder));
}
这个方法表明,使用SimpleCallback
内部类不需要重写getMovementFlags
方法,只需要传递dragDirs, swipeDirs这两个参数即可,其实和Callback
内部类差不多,如果使用Callback
内部类,那么getMovementFlags完善后的代码如下:
//声明一个Callback
ItemTouchHelper.Callback callback = new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
//控制拖拽的方向(一般是上下左右)
int dragFlags= ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
//控制快速滑动的方向(一般是左右)
int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
return makeMovementFlags(dragFlags, swipeFlags);//计算movement flag值
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
//拖拽处理
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
//滑动处理
}
};
就这样,getMovementFlags
方法已经实现完成,代码的意思是:在拖拽时,允许Item上、下、左、右四个方向拖拽,在滑动时,允许Item左、右两个方向滑动。
当然,如果您只想让Item向左滑动,那么将以下代码
int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
改成
int swipeFlags = ItemTouchHelper.LEFT;
或者
//控制快速滑动的方向(一般是左右)
int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
//排除右方向
swipeFlags &=~ItemTouchHelper.RIGHT;
即可。
有关添加状态
和移除状态
相关算法我补充一下吧,虽然不是文章的重点(其实,我怕您看不懂)
- 使用
按位或
添加状态,如:
flag |= ItemTouchHelper.RIGHT
- 使用
按位与和按位非
移除状态,如:
flag &= ~ItemTouchHelper.RIGHT
(3)onMove: 拖拽处理
方法如下:
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
//拖拽处理
return true;
}
这个方法是有返回值的,true:允许拖拽 false:禁止拖拽
假设,有一个需求,将Item拖拽到某个位置,并和当前位置的Item交换位置,这个该怎么做呢?
这个需求的处理,可以在onMove
方法中处理,因为它的三个参数recyclerView、viewHolder、target为这个需求提供了一切前提,viewHolder是被拖拽的Item,target是目标Item,只需要将viewHolder和target交换位置就可以实现这个需求,代码实现也比较简单,如下:
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
// 拖拽时,每移动一个位置就会调用一次。
if (recyclerView == null){
return false;
}
RecyclerView.Adapter adapter = recyclerView.getAdapter();
if (adapter == null){
return false;
}
if (list != null && list.size() > 0) {
//获取被拖拽的Item的Position
int from = viewHolder.getAdapterPosition();
//获取目标Item的Position
int endPosition = target.getAdapterPosition();
//交换List集合中两个元素的位置
Collections.swap(list, from, endPosition);
//交换界面上两个Item的位置
adapter.notifyItemMoved(from, endPosition);
}
return true;
}
效果如下:
(4)onSwiped: 滑动处理
onSwiped是为了实现Item的左右滑动,由于我在getMovementFlags方法中已经允许让Item左右侧滑,所以此时Item是只吃左右滑动的,效果如下:
如图所示,在界面删Item已经被删除,但是,您知道的,List数据对应的数据没有被删除,而且屏幕上显示的RecyclerView没有被刷新,这些处理需要在onSwiped方法中处理,代码如下:
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
//滑动处理
int position = viewHolder.getAdapterPosition();
if(list != null && list.size() > 0){
//删除List中对应的数据
list.remove(position);
if (recycleViewAdapter == null){
return;
}
//刷新页面
recycleViewAdapter.notifyItemRemoved(position);
}
}
};
效果如下:
(5)getMoveThreshold: 设置拖拽距离百分比
看一下以下方法:
@Override
public float getMoveThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
return .5f;
}
如果RecyclerView是垂直方向,那么当拖拽到当前Item高度的50%时开始执行onMove方法。
下面我将贴出两张图,分别表示0.5和5的情况:
返回值为0.5的情况:
返回值为4的情况:
(6)getMoveThreshold: 设置拖拽距离百分比
@Override
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
//返回值滑动消失的距离, 这里是相对于RecycleView的宽度,0.5f表示为RecycleView的宽度的一半,取值为0~1f之间
return .5f;
}
它的返回值默认为50%,一般没有特殊需求就按照默认。
(7)getSwipeEscapeVelocity: 精确的计算出滑动消失的距离
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
//返回值滑动消失的距离,滑动小于这个值不消失,大于消失,默认为屏幕的三分之一
//defaultValue默认值为120dp,转成px为 defaultValue * density
return defaultValue;
}
defaultValue默认值为120dp,转成px为defaultValue * density(density为像素缩放因子)
(8)getSwipeVelocityThreshold: 控制惯性速率
@Override
public float getSwipeVelocityThreshold(float defaultValue) {
//设置滑动速率,这里的速率是指以一定的速率滑动Item,松开时Item依然从某一个方向移动的速率(可以理解为惯性的速率)
//defaultValue默认值为800dp,转成px为 defaultValue * density
return defaultValue;
}
defaultValue默认值为800dp,转成px为 defaultValue * density(density为像素缩放因子)
(9)isLongPressDragEnabled: 是否允许长按拖拽
@Override
public boolean isLongPressDragEnabled() {
return true;
}
它的返回值默认为true。
(10)isItemViewSwipeEnabled: 是否允许左右滑动删除
@Override
public boolean isItemViewSwipeEnabled() {
return true
}
它的返回值默认为true。
(11)onSelectedChanged: 拖拽或滑动时被调用
@Override
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
}
从静止状态变为拖拽或者滑动的时候会回调该方法,参数actionState表示当前的状态。
这个方法可以用于处理在拖拽或者滑动时的UI变化。
(12)getAnimationDuration: 设置动画时间
@Override
public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
return super.getAnimationDuration(recyclerView, animationType, animateDx, animateDy);
}
当手指松开时,Item会移动到某个固定的位置,这个动作就是Item动画,而getAnimationDuration
方法可以设置动画的时间,默认为200ms或者250ms。
(13)clearView: 初始化话,清空View的状态
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
}
当拖拽或滑动动画结束时,也就是手指松开并等待动画结束时会执行clearView
方法,这个方法可以用来初始化数据。
(14)onChildDraw: 在Item底部绘制图形
@Override
public void onChildDraw(@NonNull Canvas canvas, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
onChildDraw方法有如下几个参数:
- canvas: 画布,可以用于绘制图形
- recyclerView:RecyclerView对象
- viewHolder:被操作的Item
- dX:Item被操作时的X轴方向的偏移量
- dY:Item被操作时的Y轴方向的偏移量
- actionState:移动状态,在前文已经给出移动的三种状态:
无操作状态
、拖拽状态
、滑动状态
- isCurrentlyActive:滑动分为两类。其一:手指滑动;其二:Item位移动画滑动;isCurrentlyActive可以用于判断是哪种滑动,如果isCurrentlyActive为true则表示手指滑动
我们看一下以下代码
private Paint paint;
private RectF rect;
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
//Item底部底部绘制图形
c.translate(-dX, 0);
paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
//在布局中,我设置的是4dp,这里需要转成px
float padding = 4 * getResources().getDisplayMetrics().density + 0.5f;
//获取X轴Item位置
float x = viewHolder.itemView.getX() + padding;
float y = viewHolder.itemView.getY() + padding;
float width = viewHolder.itemView.getWidth() - 2 * padding;
float height = viewHolder.itemView.getHeight() - 2 * padding;
rect = new RectF(x, y, x + width, y + height);
c.drawRect(rect, paint);
}
代码的意思也很简单,就是使用Canvas绘制一个矩形,演示效果如下:
(15)onChildDrawOver: 在Item上面绘制图形
@Override
public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
onChildDrawOver方法有如下几个参数:
- canvas: 画布,可以用于绘制图形
- recyclerView:RecyclerView对象
- viewHolder:被操作的Item
- dX:Item被操作时的X轴方向的偏移量
- dY:Item被操作时的Y轴方向的偏移量
- actionState:移动状态,在前文已经给出移动的三种状态:
无操作状态
、拖拽状态
、滑动状态
- isCurrentlyActive:滑动分为两类。其一:手指滑动;其二:Item位移动画滑动;isCurrentlyActive可以用于判断是哪种滑动,如果isCurrentlyActive为true则表示手指滑动
我们看一下以下代码
@Override
public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
//Item上面绘制图形
//c.translate(-dX, 0);
paint = new Paint();
paint.setColor(Color.GRAY);
paint.setStyle(Paint.Style.FILL);
//在布局中,我设置的是4dp,这里需要转成px
float padding = 4 * getResources().getDisplayMetrics().density + 0.5f;
//获取X轴Item位置
float x = viewHolder.itemView.getX() + padding;
float y = viewHolder.itemView.getY() + padding;
float width = viewHolder.itemView.getWidth() - 2 * padding;
float height = viewHolder.itemView.getHeight() - 2 * padding;
rect = new RectF(x + width - 200, y + height / 2 - 50, x + width - 100, y + height / 2 + 50);
c.drawRect(rect, paint);
}
上面代码的意思是:在Item上面画一个正方形。
效果图如下:
图中可以看到,Item上已经绘制了一个正方形。
(16)完整代码
//声明一个Callback
ItemTouchHelper.Callback callback = new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
//控制拖拽的方向(一般是上下左右)
int dragFlags= ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
//控制快速滑动的方向(一般是左右)
int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
return makeMovementFlags(dragFlags, swipeFlags);//计算movement flag值
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
// 拖拽时,每移动一个位置就会调用一次。
if (recyclerView == null){
return false;
}
RecyclerView.Adapter adapter = recyclerView.getAdapter();
if (adapter == null){
return false;
}
if (list != null && list.size() > 0) {
//获取被拖拽的Item的Position
int from = viewHolder.getAdapterPosition();
//获取目标Item的Position
int endPosition = target.getAdapterPosition();
//交换List集合中两个元素的位置
Collections.swap(list, from, endPosition);
//交换界面上两个Item的位置
adapter.notifyItemMoved(from, endPosition);
}
return true;
}
@Override
public float getMoveThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
//设置拖拽距离百分比
//如果RecyclerView是垂直方向,那么当拖拽到当前Item高度的50%时开始执行onMove方法
return .5f;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
//滑动处理
int position = viewHolder.getAdapterPosition();
if(list != null && list.size() > 0){
//删除List中对应的数据
list.remove(position);
if (recycleViewAdapter == null){
return;
}
//刷新页面
recycleViewAdapter.notifyItemRemoved(position);
}
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
}
private Paint paint;
private RectF rect;
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
//Item底部底部绘制图形
c.translate(-dX, 0);
paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
//在布局中,我设置的是4dp,这里需要转成px
float padding = 4 * getResources().getDisplayMetrics().density + 0.5f;
//获取X轴Item位置
float x = viewHolder.itemView.getX() + padding;
float y = viewHolder.itemView.getY() + padding;
float width = viewHolder.itemView.getWidth() - 2 * padding;
float height = viewHolder.itemView.getHeight() - 2 * padding;
rect = new RectF(x, y, x + width, y + height);
c.drawRect(rect, paint);
}
@Override
public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
return 1000;
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
}
@Override
public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
//Item上面绘制图形
//c.translate(-dX, 0);
paint = new Paint();
paint.setColor(Color.GRAY);
paint.setStyle(Paint.Style.FILL);
//在布局中,我设置的是4dp,这里需要转成px
float padding = 4 * getResources().getDisplayMetrics().density + 0.5f;
//获取X轴Item位置
float x = viewHolder.itemView.getX() + padding;
float y = viewHolder.itemView.getY() + padding;
float width = viewHolder.itemView.getWidth() - 2 * padding;
float height = viewHolder.itemView.getHeight() - 2 * padding;
rect = new RectF(x + width - 200, y + height / 2 - 50, x + width - 100, y + height / 2 + 50);
c.drawRect(rect, paint);
}
@Override
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
//返回值滑动消失的距离, 这里是相对于RecycleView的宽度,0.5f表示为RecycleView的宽度的一半,取值为0~1f之间
return .5f;
}
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
//返回值滑动消失的距离,滑动小于这个值不消失,大于消失,默认为屏幕的三分之一
//defaultValue默认值为120dp,转成px为 defaultValue * density
return defaultValue;
}
@Override
public float getSwipeVelocityThreshold(float defaultValue) {
//设置滑动速率,这里的速率是指以一定的速率滑动Item,松开时Item依然从某一个方向移动的速率(可以理解为惯性的速率)
//defaultValue默认值为800dp,转成px为 defaultValue * density
return defaultValue;
}
};
//创建helper对象
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
//关联recyclerView
itemTouchHelper.attachToRecyclerView(recyclerview);
以上代码只要演示一遍,基本就会明白ItemTouchHelper的作用,ItemTouchHelper常用方法是getMovementFlags
、onMove
、onSwiped
,强烈建议重点学习。
[本章完...]