网站首页 编程语言 正文
目录
- 1、效果
- 2、简介
- 3、功能拆解
- 4、功能实现
- 4.1、实现接口
- 4.1.1、getMovementFlags
- 4.1.2、onMove
- 4.1.3、onSwiped
- 4.2、绑定RecyclerView
- 4.3、设置分割线
- 4.4、选中放大/背景变色
- 4.4.1、onSelectedChanged
- 4.4.2、clearView
- 4.5、固定位置
- 4.5.1、修改adapter
- 4.5.1、修改onMove回调
- 4.5.3、重写isLongPressDragEnabled
- 4.6、其他
- 4.6.1、position
- 4.6.2、重置
- 5、源码探索
- 5.1、attachToRecyclerView
- 5.2、setupCallbacks
- 5.3、mOnItemTouchListener
- 5.3.1、select
- 5.3.2、checkSelectForSwipe
- 5.3.3、moveIfNecessary
- 5.4、startGestureDetection
- 5.4.1、ItemTouchHelperGestureListener
- 5.5、源码小结
1、效果

2、简介
本文主角是ItemTouchHelper。
它是RecyclerView对于item交互处理的一个「辅助类」,主要用于拖拽以及滑动处理。
以接口实现的方式,达到配置简单、逻辑解耦、职责分明的效果,并且支持所有的布局方式。
3、功能拆解

4、功能实现
4.1、实现接口
自定义一个类,实现ItemTouchHelper.Callback接口,然后在实现方法中根据需求简单配置即可。
class DragCallBack(adapter: DragAdapter, data: MutableList<String>) : ItemTouchHelper.Callback() {
}ItemTouchHelper.Callback必须实现的3个方法:
- getMovementFlags
- onMove
- onSwiped
其他方法还有onSelectedChanged、clearView等
4.1.1、getMovementFlags
用于创建交互方式,交互方式分为两种:
- 拖拽,网格布局支持上下左右,列表只支持上下(LEFT、UP、RIGHT、DOWN)
- 滑动,只支持前后(START、END)
最后,通过makeMovementFlags把结果返回回去,makeMovementFlags接收两个参数,dragFlags和swipeFlags,即上面拖拽和滑动组合的标志位。
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
var dragFlags = 0
var swipeFlags = 0
when (recyclerView.layoutManager) {
is GridLayoutManager -> {
// 网格布局
dragFlags = ItemTouchHelper.LEFT or ItemTouchHelper.UP or ItemTouchHelper.RIGHT or ItemTouchHelper.DOWN
return makeMovementFlags(dragFlags, swipeFlags)
}
is LinearLayoutManager -> {
// 线性布局
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
return makeMovementFlags(dragFlags, swipeFlags)
}
else -> {
// 其他情况可自行处理
return 0
}
}
}4.1.2、onMove
拖拽时回调,这里我们主要对起始位置和目标位置的item做一个数据交换,然后刷新视图显示。
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
// 起始位置
val fromPosition = viewHolder.adapterPosition
// 结束位置
val toPosition = target.adapterPosition
// 固定位置
if (fromPosition == mAdapter.fixedPosition || toPosition == mAdapter.fixedPosition) {
return false
}
// 根据滑动方向 交换数据
if (fromPosition < toPosition) {
// 含头不含尾
for (index in fromPosition until toPosition) {
Collections.swap(mData, index, index + 1)
}
} else {
// 含头不含尾
for (index in fromPosition downTo toPosition + 1) {
Collections.swap(mData, index, index - 1)
}
}
// 刷新布局
mAdapter.notifyItemMoved(fromPosition, toPosition)
return true
}4.1.3、onSwiped
滑动时回调,这个回调方法里主要是做数据和视图的更新操作。
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (direction == ItemTouchHelper.START) {
Log.i(TAG, "START--->向左滑")
} else {
Log.i(TAG, "END--->向右滑")
}
val position = viewHolder.adapterPosition
mData.removeAt(position)
mAdapter.notifyItemRemoved(position)
}4.2、绑定RecyclerView
上面接口实现部分我们已经简单写好了,逻辑也挺简单,总共不超过100行代码。
接下来就是把这个辅助类绑定到RecyclerView。
RecyclerView显示的实现就是基础的样式,就不展开了,可以查看源码。
val dragCallBack = DragCallBack(mAdapter, list)
val itemTouchHelper = ItemTouchHelper(dragCallBack)
itemTouchHelper.attachToRecyclerView(mBinding.recycleView)绑定只需要调用attachToRecyclerView就好了。
至此,简单的效果就已经实现了。下面开始优化和进阶的部分。
4.3、设置分割线
RecyclerView网格布局实现等分,我们一般先是自定义ItemDecoration,然后调用addItemDecoration来实现的。
但是我在实现效果的时候遇到一个问题,因为我加了布局切换的功能,在每次切换的时候,针对不同的布局分别设置layoutManager和ItemDecoration,这就导致随着切换次数的增加,item的间隔就越大。
addItemDecoration,顾名思义是添加,通过查看源码发现RecyclerView内部是有一个ArrayList来维护的,所以当我们重复调用addItemDecoration方法时,分割线是以递增的方式在增加的,并且在绘制的时候会从集合中遍历所有的分割线绘制。
部分源码:
@Override
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
//...
}既然知道了问题所在,也大概想到了3种解决办法:
- 调用addItemDecoration前,先调用removeItemDecoration方法remove掉之前所有的分割线
- 调用addItemDecoration(@NonNull ItemDecoration decor, int index),通过index来维护
- add时通过一个标示来判断,添加过就不添加了
好像可行,实际上并不太行...因为始终都有两个分割线实例。
我们再来梳理一下:
- 两种不同的布局
- 都有分割线
- 分割线只需设置一次
我想到另外一个办法,不对RecyclerView做处理了,既然两种布局都有分割线,是不是可以把分割线合二为一了,然后根据LayoutManager去绘制不同的分割线?
理论上是可行的,事实上也确实可以...
自定义分割线:
class GridSpaceItemDecoration(private val spanCount: Int, private val spacing: Int = 20, private var includeEdge: Boolean = false) :
RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, recyclerView: RecyclerView, state: RecyclerView.State) {
recyclerView.layoutManager?.let {
when (recyclerView.layoutManager) {
is GridLayoutManager -> {
val position = recyclerView.getChildAdapterPosition(view) // 获取item在adapter中的位置
val column = position % spanCount // item所在的列
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
if (position < spanCount) {
outRect.top = spacing
}
outRect.bottom = spacing
} else {
outRect.left = column * spacing / spanCount
outRect.right = spacing - (column + 1) * spacing / spanCount
if (position >= spanCount) {
outRect.top = spanCount
}
outRect.bottom = spacing
}
}
is LinearLayoutManager -> {
outRect.top = spanCount
outRect.bottom = spacing
}
}
}
}
}4.4、选中放大/背景变色
为了提升用户体验,可以在拖拽的时候告诉用户当前拖拽的是哪个item,比如选中的item放大、背景高亮等。
- 网格布局,选中变大
- 列表布局,背景变色
这里用到ItemTouchHelper.Callback中的两个方法,onSelectedChanged和clearView,我们需要在选中时改变视图显示,结束时再恢复。
4.4.1、onSelectedChanged
拖拽或滑动 发生改变时回调,这时我们可以修改item的视图
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
viewHolder?.let {
// 因为拿不到recyclerView,无法通过recyclerView.layoutManager来判断是什么布局,所以用item的宽度来判断
// itemView.width > 500 用这个来判断是否是线性布局,实际取值自己看情况
if (it.itemView.width > 500) {
// 线性布局 设置背景颜色
val drawable = it.itemView.background as GradientDrawable
drawable.color = ContextCompat.getColorStateList(it.itemView.context, R.color.greenDark)
} else {
// 网格布局 设置选中放大
ViewCompat.animate(it.itemView).setDuration(200).scaleX(1.3F).scaleY(1.3F).start()
}
}
}
super.onSelectedChanged(viewHolder, actionState)
}actionState:
- ACTION_STATE_IDLE 空闲状态
- ACTION_STATE_SWIPE 滑动状态
- ACTION_STATE_DRAG 拖拽状态
4.4.2、clearView
拖拽或滑动 结束时回调,这时我们要把改变后的item视图恢复到初始状态
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
// 恢复显示
// 这里不能用if判断,因为GridLayoutManager是LinearLayoutManager的子类,改用when,类型推导有区别
when (recyclerView.layoutManager) {
is GridLayoutManager -> {
// 网格布局 设置选中大小
ViewCompat.animate(viewHolder.itemView).setDuration(200).scaleX(1F).scaleY(1F).start()
}
is LinearLayoutManager -> {
// 线性布局 设置背景颜色
val drawable = viewHolder.itemView.background as GradientDrawable
drawable.color = ContextCompat.getColorStateList(viewHolder.itemView.context, R.color.greenPrimary)
}
}
super.clearView(recyclerView, viewHolder)
}4.5、固定位置
在实际需求中,交互可能要求我们第一个菜单不可以变更顺序,只能固定,比如效果中的第一个菜单「推荐」固定在首位这种情况。
4.5.1、修改adapter
定义一个固定值,并设置不同的背景色和其他菜单区分开。
class DragAdapter(private val mContext: Context, private val mList: List<String>) : RecyclerView.Adapter<DragAdapter.ViewHolder>() {
val fixedPosition = 0 // 固定菜单
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.mItemTextView.text = mList[position]
// 第一个固定菜单
val drawable = holder.mItemTextView.background as GradientDrawable
if (holder.adapterPosition == 0) {
drawable.color = ContextCompat.getColorStateList(mContext, R.color.greenAccent)
}else{
drawable.color = ContextCompat.getColorStateList(mContext, R.color.greenPrimary)
}
}
//...
}4.5.1、修改onMove回调
在onMove方法中判断,只要是固定位置就直接返回false。
class DragCallBack(adapter: DragAdapter, data: MutableList<String>) : ItemTouchHelper.Callback() {
/**
* 拖动时回调
*/
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
// 起始位置
val fromPosition = viewHolder.adapterPosition
// 结束位置
val toPosition = target.adapterPosition
// 固定位置
if (fromPosition == mAdapter.fixedPosition || toPosition == mAdapter.fixedPosition) {
return false
}
// ...
return true
}
}虽然第一个菜单无法交换位置了,但是它还是可以拖拽的。
效果实现了吗,好像也实现了,可是又好像哪里不对,就好像填写完表单点击提交时你告诉我格式不正确一样,你不能一开始就告诉我吗?
为了进一步提升用户体验,可以让固定位置不可以拖拽吗?
可以,ItemTouchHelper.Callback中有两个方法:
- isLongPressDragEnabled 是否可以长按拖拽
- isItemViewSwipeEnabled 是否可以滑动
这俩方法默认都是true,所以即使不能交换位置,但默认也是支持操作的。
4.5.3、重写isLongPressDragEnabled
以拖拽举例,我们需要重写isLongPressDragEnabled方法把它禁掉,然后再非固定位置的时候去手动开启。
override fun isLongPressDragEnabled(): Boolean {
//return super.isLongPressDragEnabled()
return false
}禁掉之后什么时候再触发呢?
因为我们现在的交互是长按进入编辑,那就需要在长按事件中再调用startDrag手动开启
mAdapter.setOnItemClickListener(object : DragAdapter.OnItemClickListener {
//...
override fun onItemLongClick(holder: DragAdapter.ViewHolder) {
if (holder.adapterPosition != mAdapter.fixedPosition) {
itemTouchHelper.startDrag(holder)
}
}
})ok,这样就完美实现了。
4.6、其他
4.6.1、position
因为有拖拽操作,下标其实是变化的,在做相应的操作时,要取实时位置
holder.adapterPosition
4.6.2、重置
不管是拖拽还是滑动,其实本质都是对Adapter内已填充的数据进行操作,实时数据通过Adapter获取即可。
如果想要实现重置功能,直接拿最开始的原始数据重新塞给Adapter即可。
5、源码探索
看源码时,找对一个切入点,往往能达到事半功倍的效果。
这里就从绑定RecyclerView开始吧
val dragCallBack = DragCallBack(mAdapter, list)
val itemTouchHelper = ItemTouchHelper(dragCallBack)
itemTouchHelper.attachToRecyclerView(mBinding.recycleView)实例化ItemTouchHelper,然后调用其attachToRecyclerView方法绑定到RecyclerView。
5.1、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();
}
}这段代码其实有点意思的,解读一下:
- 第一个if判断,避免重复操作,直接return
- 第二个if判断,调用了destroyCallbacks,在destroyCallbacks里面做了一些移除和回收操作,说明只能绑定到一个RecyclerView;同时,注意这里判断的主体是mRecyclerView,不是我们传进来的recyclerView,而且我们传进来的recyclerView是支持Nullable的,所以我们可以传个空值走到destroyCallbacks里来做解绑操作
- 第三个if判断,当我们传的recyclerView不为空时,调用setupCallbacks
5.2、setupCallbacks
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.addOnChildAttachStateChangeListener(this);
startGestureDetection();
}这个方法里已经大概可以看出内部实现原理了。
两个关键点:
- addOnItemTouchListener
- startGestureDetection
通过触摸和手势识别来处理交互显示。
5.3、mOnItemTouchListener
private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
mGestureDetector.onTouchEvent(event);
if (action == MotionEvent.ACTION_DOWN) {
//...
if (mSelected == null) {
if (animation != null) {
//...
select(animation.mViewHolder, animation.mActionState);
}
}
} else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
select(null, ACTION_STATE_IDLE);
} else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
//...
if (index >= 0) {
checkSelectForSwipe(action, event, index);
}
}
return mSelected != null;
}
@Override
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
mGestureDetector.onTouchEvent(event);
//...
if (activePointerIndex >= 0) {
checkSelectForSwipe(action, event, activePointerIndex);
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (activePointerIndex >= 0) {
moveIfNecessary(viewHolder);
}
break;
}
//...
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
select(null, ACTION_STATE_IDLE);
}
};这段代码删减之后还是有点多,不过没关系,提炼一下,核心通过判断MotionEvent调用了几个方法:
- select
- checkSelectForSwipe
- moveIfNecessary
5.3.1、select
void select(@Nullable ViewHolder selected, int actionState) {
if (selected == mSelected && actionState == mActionState) {
return;
}
//...
if (mSelected != null) {
if (prevSelected.itemView.getParent() != null) {
final float targetTranslateX, targetTranslateY;
switch (swipeDir) {
case LEFT:
case RIGHT:
case START:
case END:
targetTranslateY = 0;
targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
break;
//...
}
//...
} else {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
mCallback.clearView(mRecyclerView, prevSelected);
}
}
//...
mCallback.onSelectedChanged(mSelected, mActionState);
mRecyclerView.invalidate();
}这里面主要是在拖拽或滑动时对translateX/Y的计算和处理,然后通过mCallback.clearView和mCallback.onSelectedChanged回调给我们,最后调用invalidate()实时刷新。
5.3.2、checkSelectForSwipe
void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
//...
if (absDx < mSlop && absDy < mSlop) {
return;
}
if (absDx > absDy) {
if (dx < 0 && (swipeFlags & LEFT) == 0) {
return;
}
if (dx > 0 && (swipeFlags & RIGHT) == 0) {
return;
}
} else {
if (dy < 0 && (swipeFlags & UP) == 0) {
return;
}
if (dy > 0 && (swipeFlags & DOWN) == 0) {
return;
}
}
select(vh, ACTION_STATE_SWIPE);
}这里是滑动处理的check,最后也是收敛到select()方法统一处理。
5.3.3、moveIfNecessary
void moveIfNecessary(ViewHolder viewHolder) {
if (mRecyclerView.isLayoutRequested()) {
return;
}
if (mActionState != ACTION_STATE_DRAG) {
return;
}
//...
if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
// keep target visible
mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
target, toPosition, x, y);
}
}这里检查拖拽时是否需要交换item,通过mCallback.onMoved回调给我们。
5.4、startGestureDetection
private void startGestureDetection() {
mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
mItemTouchHelperGestureListener);
}5.4.1、ItemTouchHelperGestureListener
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
//...
@Override
public void onLongPress(MotionEvent e) {
//...
View child = findChildView(e);
if (child != null) {
ViewHolder vh = mRecyclerView.getChildViewHolder(child);
if (vh != null) {
//...
if (pointerId == mActivePointerId) {
//...
if (mCallback.isLongPressDragEnabled()) {
select(vh, ACTION_STATE_DRAG);
}
}
}
}
}
}这里主要是对长按事件的处理,最后也是收敛到select()方法统一处理。
5.5、源码小结
- 绑定RecyclerView
- 注册触摸手势监听
- 根据手势,先是内部处理各种校验、位置计算、动画处理、刷新等,然后回调给ItemTouchHelper.Callback
事儿大概就是这么个事儿,主要工作都是源码帮我们做了,我们只需要在回调里根据结果处理业务逻辑即可。
原文链接:https://juejin.cn/post/7124354102296838151
相关推荐
- 2022-03-26 C语言实现字符串替换的示例代码_C 语言
- 2022-05-23 .NET中堆栈和堆的特点与差异介绍_实用技巧
- 2022-01-26 关于(NOTICE)iconv(): Detected an illegal character i
- 2022-09-06 C++详细讲解引用类型_C 语言
- 2022-11-02 Python中turtle库常用代码汇总_python
- 2022-10-28 Django静态文件配置request对象方法ORM操作讲解_python
- 2022-08-18 详解Flutter中数据传递的方式_Android
- 2023-04-14 使用 React hooks 实现类所有生命周期_React
- 最近更新
-
- window11 系统安装 yarn
- 超详细win安装深度学习环境2025年最新版(
- Linux 中运行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存储小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基础操作-- 运算符,流程控制 Flo
- 1. Int 和Integer 的区别,Jav
- spring @retryable不生效的一种
- Spring Security之认证信息的处理
- Spring Security之认证过滤器
- Spring Security概述快速入门
- Spring Security之配置体系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置权
- redisson分布式锁中waittime的设
- maven:解决release错误:Artif
- restTemplate使用总结
- Spring Security之安全异常处理
- MybatisPlus优雅实现加密?
- Spring ioc容器与Bean的生命周期。
- 【探索SpringCloud】服务发现-Nac
- Spring Security之基于HttpR
- Redis 底层数据结构-简单动态字符串(SD
- arthas操作spring被代理目标对象命令
- Spring中的单例模式应用详解
- 聊聊消息队列,发送消息的4种方式
- bootspring第三方资源配置管理
- GIT同步修改后的远程分支
