TaskListDemo的升级----使用事件分发添加侧滑显示删除按钮
前言
之前写过一篇关于RecyclerView的进阶使用来搭建一个简易的ToDoList
列表(link),里面是通过侧滑滑倒底直接删除,但这样显然不够美观,也容易导致误触。我们可以像QQ那样实现一个侧滑到一定程度显示一个按钮,我们点击按钮进行删除,比如:

那这个又应该如何实现呢?
实现
我们可以设置一个LinearLayout
布局,让text占满屏幕,让按钮隐藏在屏幕外,然后通过整个布局的onTouchEvent()
方法,来改变位置
1 | parentLayout.setOnTouchListener { v, event -> |
MotionEvent.ACTION_DOWN
指手指按下去时,这时我们去获取他的初始位置;MotionEvent.ACTION_MOVE
表示手指开始滑动时,我们用event.x - initialX
来获取他的偏移值,并用coerceIn
决定他的最大最小值,因为向左滑他的偏移值是负的。然后我们将文字和删除按钮的偏移值同步设置达到一个将删除键拉出来的效果。
注意这里不能直接
parentLayout.translationX = transitionX
,因为此时delete
按钮被canvas
会裁剪掉。
但这样又会有许多的问题,比如你会发现点击事件不能用了,当你滑出来后再滑回去会一下次跳回去。
问题解决
解决无法点击的问题
这时如果我们给textLayout
或者View
设置一个setOnClickListener
,是不会生效的,这是为什么呢?
点击事件不生效是因为事件分发的问题,先看一下事件分发的流程图:

看上面的DOWN
部分,子View
的dispatchTouchEvent
方法传到onTouchEvent
时,因为我们处理了滑动事件,返回了true
,导致父类和子类会认为已经消费了点击事件,故不会处理父类和子类的点击事件,即不会从子类的onTouchEvent
到父类的onTouchEvent
,故父类的view无法被点击。
gpt回答:
触摸事件的消费:
- 当你在
onTouchEvent
中处理滑动事件时,如果你消费了事件(即返回true
),那么点击事件就不会再传递到其他视图或父视图。- 如果你在
onTouchEvent
中处理了ACTION_MOVE
事件并返回true
,但没有正确处理ACTION_UP
事件,点击事件可能会被视为未完成。事件冲突:
- 当一个视图处理滑动事件时,父视图可能会认为子视图已经消费了事件,从而不会再处理点击事件。
- 如果父视图拦截了触摸事件(
onInterceptTouchEvent
返回true
),子视图将无法接收到后续的事件。
那么如何解决呢?我们可以在父View
的dispatchTouchEvent
传到子View
的dispatchTouchEvent
时,检测如果滑动距离大于slopTouch
(这是官方提供的滑动距离的最小值),我们就认为此时应该处理滑动,因此重写onInterceptTouchEvent
返回true
去拦截向下分发事件的这个操作,从而让父类自己处理触摸事件,也就是调用父类自己的onTouchEvent
。相反,如果滑动距离小于slopTouch
,那么就是分发给子类去处理点击事件,即处理子类的onTouchEvent
。
总结一下流程:
代码实现应该通过重写LinearLayout
布局来解决:
1 | class ListLinearLayout(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) { |
然后外部布局替换成自定义的LinearLayout
就可以了。然后在Adapter中就可以删除所有关于onTouchEvent
的代码了。
1 | private val slopTouch = ViewConfiguration.get(context).scaledTouchSlop |
这一行代码就是获取官方推荐的滑动值。
在一个父类
View
只有两个子布局的情况下,可以通过getChildAt(1),getChildAt(0)
的方法快速获取子View
的实例,等同于findViewById
。如:
1
2 val parentDelete = getChildAt(1)
val textLayout = getChildAt(0)
这里面
onTouchEvent
的DOWN
事件里面不能处理initialX
,因为这里的DOWN
事件需要子View
不拦截onTouchEvent
才能向上传到父类,而子View
的点击事件拦截了这个DOWN
的onTouchEvent
,所以父View
无法接受DOWN
事件,故需要在上面onInterceptTouchEvent
这里面处理初始位置,否则是滑不出来的。
解决滑回去时突然一下子滑回去的问题
这个问题是因为在初始化坐标时initialX = event.x.toInt()
,已经滑出来的状态下,此时initialX = 90
假设我们只滑动很小一部分,,因为是往回滑动,假设滑倒x = 95
,那么此时差值就只有5
,根据代码就会将translationX
设为5
,趋近于没滑动,故直接跳到初始位置,即不展现的状态了。
解决方式也很简单,我们初始化initialX
时 - getChildAt(1).translationX.toInt()
就可以了,因为相当于我们要回到真正的初始位置需要向右移动,而getChildAt(1).translationX.toInt()
是负数,故需要减来让他回到正确的初始位置而不是滑出来的位置。
给rv添加长按交换位置
因为我们自定义了滑动,所以正常rv封装的滑动是不能用了,但我们不能简简单单的删除onSwipe
方法,因为是必须要重写的,我们只是留空。
1 | val helper = ItemTouchHelper(object : ItemTouchHelper.Callback() { |
在第6到第7行的位置我们删除了val swipeFlags = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
这一句,然后将第7行的return
由return makeMovementFlags(dragFlags, swipeFlags)
改成了return makeFlag(ACTION_STATE_DRAG,dragFlags) or makeFlag(ACTION_STATE_IDLE, dragFlags)
,这个makeFlag
意思是当前的状态为滑动时,就允许上下滑动,然后加了个位运算空闲状态下也允许上下动(可以不加后面这个),这样就能只进行上下拖动了。
总结
没想到这一个小demo
能引出这么多的知识,设计了rv的进阶用法,事件分发冲突以及各种零碎的小问题。还是获益匪浅的。
github地址:https://github.com/generalio/AndroidStudy/tree/main/TaskListDemo