本篇为系列文章
一个跟随手指滑动的图片 – 单点触摸
自定义 View:多点触控 (一)-- 手指接力
自定义View:多点触控(二)-- 多指协作
自定义 View:多点触控 (三)-- 各自为战
多点触控大致可分为三种类型:
- 接力型
- 协作型
- 各自为战
那么,默认的多指操作是上面的哪一种呢,很遗憾,都不是,我们可以运行我们上一篇文章讲的,一个可拖动的图片,这里面没有处理多指操作,当我们两个手指同时进行操作的时候,会发现效果是这样:第一个手指正常操作,第二个手指放上去开始滑动但是没有反应,只有当第一个手指移除,才会响应第二个手指的操作,当第一个手指放回时,第二个手指立即失效,直接响应第一个手指的操作
为什么会这样呢,在讲这个之前,先要说一个概念,单指操作的事件序列
一个最简单的事件序列通常是这样的
ACTION_DOWN
ACTION_MOVE
ACTION_MOVE
ACTION_MOVE
ACTION_MOVE
ACTION_MOVE
ACTION_MOVE
ACTION_MOVE
ACTION_MOVE
ACTION_UP
从按下到抬起,算作一次事件,而ACTION_MOVE是会多次触发的,因为哪怕是你以为你没动,但其实在屏幕上哪怕是微微的晃动都会触发
还有一种是ACTION_CANNEL不在讨论当中,因为他不是由用户触发的,是由父View触发的,当事件被父View抢夺就会触发被抢夺View的ACTION_CANNEL,是让子View处理后事,也就是擦屁股用的
那么多指操作的事件序列是怎么样的呢?
ACTION_DOWN
ACTION_MOVE
ACTION_MOVE
ACTION_POINTER_DOWN
ACTION_MOVE
ACTION_MOVE
ACTION_POINTER_DOWN
ACTION_MOVE
ACTION_POINTER_UP
ACTION_MOVE
ACTION_POINTER_UP
ACTION_MOVE
ACTION_MOVE
ACTION_UP
每多一个手指会一对ACTION_POINTER_DOWN 和 ACTION_POINTER_UP
注意,以上每一个序列都是站在一个View的角度来说的,而不是站在不同的手指的角度
是同一个View的多个手指,会在同一个事件序列里面,就像以上所描述的那样
每一个事件都会对应一个坐标,而事件是由手指触发的,也就是对应一个point, 也是站在View的角度来说,是针对View的
那么不同的手指如何区分呢
ACTION_DOWN p(x,y,index,id)
ACTION_MOVE p(x,y,index,id)
ACTION_MOVE p(x,y,index,id)
ACTION_POINTER_DOWN p(x,y,index,id) p(x,y,index,id)
ACTION_MOVE p(x,y,index,id) p(x,y,index,id)
ACTION_MOVE p(x,y,index,id) p(x,y,index,id)
ACTION_POINTER_DOWN
ACTION_MOVE
ACTION_POINTER_UP
ACTION_MOVE
ACTION_POINTER_UP
ACTION_MOVE
ACTION_MOVE
ACTION_UP
看以上序列集,当有一个**p()的时候,代表这个View有一个手指在操作,当有两个p()**时,代表有两个手指在操作
上代码,基于上一篇的单指操作进行改造
import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import com.example.viewtest.R
class MultiTouchView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_choice_select)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var offsetX = 0f
private var offsetY = 0f
private var downX = 0f
private var downY = 0f
private var originOffsetX = 0f
private var originOffsetY = 0f
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// 绘制时候使用更新后的坐标
canvas?.drawBitmap(bitmap, offsetX, offsetY, paint)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.actionMasked) {
MotionEvent.ACTION_DOWN -> {
// 记录手指按下的坐标
downX = event.x
downY = event.y
}
MotionEvent.ACTION_MOVE -> {
// 记录最新的手指位置
offsetX = event.x - downX + originOffsetX
offsetY = event.y - downY + originOffsetY
// 触发重绘
invalidate()
}
MotionEvent.ACTION_UP -> {
originOffsetX = offsetX
originOffsetY = offsetY
}
}
return true
}
}
这里需要注意的是:
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.actionMasked) {
}
}
使用 event?.actionMasked 来处理多指操作事件
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.actionMasked) {
MotionEvent.ACTION_DOWN -> {
trackingPointerId = event.getPointerId(0)
// 记录手指按下的坐标
downX = event.x
downY = event.y
}
// 新增的手指按下会触发
MotionEvent.ACTION_POINTER_DOWN -> {
val actionIndex = event.actionIndex
trackingPointerId = event.getPointerId(actionIndex)
// 记录新手指按下的坐标
downX = event.getX(actionIndex)
downY = event.getY(actionIndex)
originOffsetX = offsetX
originOffsetY = offsetY
}
MotionEvent.ACTION_MOVE -> {
val index = event.findPointerIndex(trackingPointerId)
// 记录最新的手指位置
offsetX = event.getX(index) - downX + originOffsetX
offsetY = event.getY(index) - downY + originOffsetY
// 触发重绘
invalidate()
}
MotionEvent.ACTION_UP -> {
originOffsetX = offsetX
originOffsetY = offsetY
}
}
return true
}
这里只看onTouchEvent的相关代码,这里新增了ACTION_POINTER_UP类型,然后所有的坐标获取都改成了使用getX或者getY了,之前写的其实默认的index是0而已,而现在的index是通过id获取的,这也就解释了事件序列集中的所有参数的作用
到此为止,我们运行,结果第二根手指的事件接管是好使的
然后我们需要处理非最后一根手指抬起的操作,其实我们处理非第一根手指的按下或者抬起,都是在处理接棒的问题,最后一根手指没人接了,就不需要考虑了
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.actionMasked) {
......
// 当前 view 上所剩手指大于 1 时,有手指抬起会触发
MotionEvent.ACTION_POINTER_UP -> {
val actionIndex = event.actionIndex
val pointerId = event.getPointerId(actionIndex)
// 判断抬起手指的 ID 是否是正在操作的手指 ID,如果是,则需要选一根手指来接管操作
if (pointerId == trackingPointerId) {
// 需要选一根手指来接管操作,具体选哪个手指,需要我们自己写算法,这里选最后一根手指
// 注意,这个时候去获取当前View的所有手指的时候,是包括当前正在抬起的手指的
// 如果抬起的手指就是最后一根手指,那么我自己是不可能接棒我自己的
val newIndex = if (actionIndex == event.pointerCount - 1) {
event.pointerCount - 2
} else {
event.pointerCount - 1
}
trackingPointerId = event.getPointerId(newIndex)
downX = event.getX(newIndex)
downY = event.getY(newIndex)
originOffsetX = offsetX
originOffsetY = offsetY
}
}
.....
}
return true
}
还是看onTouchEvent方法,这里只看ACTION_POINTER_UP事件,看注释描述
运行看结果:
- 第一根手指按下移动、第二根手指按下移动,第二根手指顺利接管,第一根手指失效,第一根手指先抬起,剩下的手指仍然正常操纵,没有问题
- 第一根手指按下移动、第二根手指按下移动,第二根手指顺利接管,第一根手指失效,第二根手指先抬起,剩下的手指仍然正常操纵,没有问题
完美实现多指操作类型中的第一种类型,接力类型