一.前言
在开发中,我们经常会使用自定义View进行拖动和缩放,拖动一般是单指触摸,但是实际情况我们也可以使用多指进行触摸移动,如果这是我们进行抬起
二.了解触摸事件
2.1 了解MotionEvent中手指的index和id
在MotionEvent中,存储了每个手指的index和id:
- Index: 每个MotionEvent在数组中存储了手指的信息,index表示手指在数组中的位置,开发中我们也是通过index作为参数来获取相关信息,比如event.getX(int pointerIndex),event.getY(int pointerIndex)来获取MotionEvent的坐标信息,但是当有多个手指放在屏幕上,同时又抬起相关的手指,那么这个pointerIndex会发生改变。
- ID:每个手指都有一个ID映射,它在整个触摸中保持持久性。
2.2 了解MotionEvent中Event
当多个手指同时触摸屏幕的时候,系统会生成以下事件:
- ACTION_DOWN:当第一个手指触摸到屏幕的时候,生成事件。在该事件中pointerIndex始终为0.
- ACTION_POINTER_DOWN:出了第一个触摸屏幕外的手指触摸屏幕的时候会受到该事件,该事件的pointerIndex可通过event.getActionIndex()获取。
- ACTION_MOVE:当手指按下的过程中发生移动
- ACTION_POINTER_UP:不是主的手指抬起
- ACTION_UP:当最后一个手指离开屏幕
2.3 了解MotionEvent的事件流程
每个手指在MotionEvent中的顺序是为定义的。因此,手指保持活动的状态下,index是可以从一个事件更改到下一个事件的。我们需要使用event.getPointerId()来来获取手指的Id,以便在后续的MotionEvent中追踪对应的手指,再通过findPointerIdnex()来获取对应手指ID的在MotionEvent中的index。
针对上述这个描述我们举个实例来说明一下,定义一个View,进行拖动处理,代码如下:
class MoveView : View {
constructor(context: Context?) : super(context)
constructor(context: Context?, attributeSet: AttributeSet) : super(context, attributeSet)
constructor(context: Context?, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)
private var mLastTouchX = 0f
private var mLastTouchY = 0f
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mLastTouchX = event.x
mLastTouchY = event.y
}
MotionEvent.ACTION_MOVE -> {
translationX = translationX + event.x - mLastTouchX
translationY = translationY + event.y - mLastTouchY
}
}
return true
}
}
单个手指触摸在在MoveView上的时候正常,但当有个两个手指在屏幕上,手指不停的切换抬起和按下的时候会出现如下效果:
这种效果对应一个有追求的程序员肯定是不能满足的。操作上面的这个View,会感觉抖动和不平滑。那么出现这个问题的原生是什么呢?修改一下代码,将事件的日志输出:
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
val actionIndex = event.actionIndex
val mActivePointerId = event.getPointerId(actionIndex)
mLastTouchX = event.x
mLastTouchY = event.y
log("MotionEvent.ACTION_DOWN:-- actionIndex:$actionIndex---activePointerId:$mActivePointerId")
}
MotionEvent.ACTION_MOVE -> {
translationX = translationX + event.x - mLastTouchX
translationY = translationY + event.y - mLastTouchY
}
MotionEvent.ACTION_UP -> {
log("MotionEvent.ACTION_UP ")
}
MotionEvent.ACTION_POINTER_DOWN -> {
val actionIndex = event.actionIndex
val mActivePointerId = event.getPointerId(actionIndex)
log("MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:$actionIndex---activePointerId:$mActivePointerId")
}
MotionEvent.ACTION_POINTER_UP -> {
val actionIndex = event.actionIndex
val mActivePointerId = event.getPointerId(actionIndex)
log("MotionEvent.ACTION_POINTER_UP:-- actionIndex:$actionIndex---activePointerId:$mActivePointerId")
}
}
return true
}
得到如下日志:
D/MoveView: MotionEvent.ACTION_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:1---activePointerId:1
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:1---activePointerId:1
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:1---activePointerId:1
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:1---activePointerId:1
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:1---activePointerId:1
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:1---activePointerId:1
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:1---activePointerId:1
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:1---activePointerId:1
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:1---activePointerId:1
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_DOWN:-- actionIndex:0---activePointerId:0
D/MoveView: MotionEvent.ACTION_POINTER_UP:-- actionIndex:0---activePointerId:0
从上面的日志可以看出,最后几行的日志输出,屏幕上已经有了一个手指,但是再按下一个手指的时候,分配的actionIndex为0,理想情况应该是1,这里就导致了我们在ACTION_MOVE中使用event.getX()和event.getY()取出的坐标是当前手指按下的坐标以及mLastTouchX和mLastTouchY是上一个手指ACTION_DOWN时的值,因此计算的结果就不是我们期望的值,直接的效果就是上图中的抖动效果。因此这里这里我们需要处理MotionEvent.ACTION_POINTER_UP的手指ID,在evnet.getX()和event.getY()中传入id对应的手指index。代码如下:
//取出当前事件分配的手指Index
val pointerIndex = event.actionIndex
//根据索引得到手指ID
val pointerId = event.getPointerId(pointerIndex)
//
if (pointerId == mActivePointerId) {
//从上图的日志的后几行中,我可以看出,当屏幕上有一个手指,再次按下和抬起的时候,分配的手指index为0,这里我们就需要或index为1的手指索引
val newPointerIndex = if (pointerIndex == 0) 1 else 0
//重新改变按下触摸点的坐标
mLastTouchX = event.getX(newPointerIndex)
mLastTouchY = event.getY(newPointerIndex)
//获取新activiePointerId
mActivePointerId = event.getPointerId(newPointerIndex)
}
完整代码:
private val INVALID_POINTER_ID = -1
private var mActivePointerId = INVALID_POINTER_ID
private var mLastTouchX = 0f
private var mLastTouchY = 0f
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
val pointerIndex = event.actionIndex
mActivePointerId = event.getPointerId(0)
mLastTouchX = event.getX(pointerIndex)
mLastTouchY = event.getY(pointerIndex)
}
MotionEvent.ACTION_MOVE -> {
val pointerIndex = event.findPointerIndex(mActivePointerId)
translationX = translationX + event.getX(pointerIndex) - mLastTouchX
translationY = translationY + event.getY(pointerIndex) - mLastTouchY
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
mActivePointerId = INVALID_POINTER_ID
}
MotionEvent.ACTION_POINTER_UP -> {
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
if (pointerId == mActivePointerId) {
val newPointerIndex = if (pointerIndex == 0) 1 else 0
mLastTouchX = event.getX(newPointerIndex)
mLastTouchY = event.getY(newPointerIndex)
mActivePointerId = event.getPointerId(newPointerIndex)
}
}
}
return true
}
}
三.总结
- 多指触摸事件需要使用actionMasked来进行判断处理
- 如果开发中遇到类似的滑动问题,可以参考ScollView,RecycleView等的处理方式