一、基础准备
1.1、MotionEvent.getActionMasked()
MotionEvent.getAction() 只能用于单指,考虑多指触控时需要用 MotionEvent.getActionMasked()。常见值:
- ACTION_DOWN:第一个手指按下(之前没有任何手指触摸到 View)
- ACTION_UP:最后一个手指抬起(抬起之后没有任何手指触摸到 View,这个手指未必是 ACTION_DOWN 的那个手指)
- ACTION_MOVE:有手指发生移动
- ACTION_POINTER_DOWN:额外手指按下(按下之前已经有别的手指触摸到 View)
- ACTION_POINTER_UP:有手指抬起,但不是最后一个(抬起之后,仍然还有别的手指在触摸着 View)
1.2、触摸事件的结构
- 触摸事件是按序列来分组的,每一组事件必然以 ACTION_DOWN 开头,以 ACTION_UP 或ACTION_CANCEL 结束。
- ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 和 ACTION_MOVE 一样,只是事件序列中的组成部分,并不会单独分出新的事件序列
- 触摸事件序列是针对 View 的,而不是针对 pointer 的。「某个 pointer 的事件」这种说法是不正确的。
- 同一时刻,一个 View 要么没有事件序列,要么只有一个事件序列。
1.3、多点触控的三种类型
- 接力型:同一时刻只有一个 pointer 起作用,即最新的 pointer。 典型: ListView、RecyclerView。 实现方式:在 ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 时记录下最新的 pointer,在之后的 ACTION_MOVE 事件中使用这个 pointer 来判断位置。
- 配合型 / 协作型 所有触摸到 View 的 pointer 共同起作用。典型: ScaleGestureDetector,以及 GestureDetector 的 onScroll() 方法判断。 实现方式:在每个 DOWN、 POINTER_DOWN、 POINTER_UP、 UP 事件中使用所有 pointer 的坐标来共同更新焦点坐标,并在 MOVE 事件中使用所有 pointer 的坐标来判断位置。
- 各自为战型:各个 pointer 做不同的事,互不影响。 典型:支持多画笔的画板应用。 实现方式:在每个 DOWN、 POINTER_DOWN 事件中记录下每个 pointer 的 id,在 MOVE 事件中使用 id 对它们进行跟踪。
下面我们会分别看一下这三种类型的多点触控如何使用。
1.4、actionIndex 和 pointerId
多指触摸时,MotionEvent 会为每个手指分配一个 actionIndex 和 pointerId:
- actionIndex:代表手指序号,它是会变的,序号从 0 开始。一般用来循环遍历我们的手指。
- pointerId:它是不会变的,一般用来追踪手指。
举个例子,第一个手指触摸时,它的 actionIndex 和 pointerId 都为 0,新增第二个手指触摸时,它的 actionIndex 都为 1:
第一个手指 | 第二个手指 | |
---|---|---|
actionIndex | 0 | 1 |
pointerId | 0 | 1 |
此时第一个手指离开屏幕后:
第一个手指(离开) | 第二个手指 | |
---|---|---|
actionIndex | 0 | |
pointerId | 1 |
可以看到,第二个手指的 actionIndex 变成了 0,而 pointerId 没变。
actionIndex 的获取可以直接用 MotionEvent.getActionIndex() 方法获取,而 pointerId 需要用 actionIndex 来获取 :event.getPointerId(actionIndex)。通过 pointerId 也可以获取 actionIndex :findPointerIndex(int pointerId)。
另外,在 View 地 onTouchEvent 方法中,我们平常用 event.getX()、event.getY() 是获取第一个手指的 x 和 y。而如果要获取其他手指,则需要用 getX(int pointerIndex)、getY(int pointerIndex) 获取指定手指的 x、y。
二、接力型
接力型的效果是这样的,比如一个 RecyclerView,第一个手指在上面滑动一段距离后,此时第二个手指也放上来了,那么此时由第二个手指控制滑动,第二个手指松开后,又交由第一个手机控制滑动。简单地说就是同一时刻只有最新地那个手指能起作用地。我们先看一下实现代码:
class MultiTouchView1(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
companion object {
val IMAGE_WIDTH = Utils.dp2px(200)
}
private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var mBitmap: Bitmap
//手指按下时的坐标
private var mDownPoint = PointF()
//canvas偏移坐标
private var mCanvasOffsetPoint = PointF()
//canvas上一次偏移坐标
private var mCanvasLastOffsetPoint = PointF()
//当前正在监控的手指
private var mTrackingPointerId = 0
init {
mBitmap = Utils.decodeBitmap(resources, R.drawable.avatar, IMAGE_WIDTH.toInt())
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
setTrackingPointer(event.actionIndex, event)//分析1
}
MotionEvent.ACTION_MOVE -> {
val index = event.findPointerIndex(mTrackingPointerId)
mCanvasOffsetPoint.x = mCanvasLastOffsetPoint.x + event.getX(index) - mDownPoint.x
mCanvasOffsetPoint.y = mCanvasLastOffsetPoint.y + event.getY(index) - mDownPoint.y
invalidate()
}
MotionEvent.ACTION_POINTER_UP -> {
//抬起手指时切换监控的手指
//当前抬起的手指index
val actionIndex = event.actionIndex
//当前抬起的手指id
val pointerId = event.getPointerId(actionIndex)
//看抬起的手指是否是当前正在监控的手指
if (pointerId == mTrackingPointerId) {
//判断抬起的手指是否是最后一个,并将当前追踪点设置为最后一个index对应的point
val newIndex =
if (actionIndex == event.pointerCount - 1) event.pointerCount - 2 else event.pointerCount - 1//分析2
setTrackingPointer(newIndex, event)
}
}
}
return true
}
/**
* 设置当前追踪的 Pointer
*/
private fun setTrackingPointer(newPointIndex: Int, event: MotionEvent) {
mTrackingPointerId = event.getPointerId(newPointIndex)
mDownPoint.x = event.getX(newPointIndex)
mDownPoint.y = event.getY(newPointIndex)
mCanvasLastOffsetPoint.x = mCanvasOffsetPoint.x
mCanvasLastOffsetPoint.y = mCanvasOffsetPoint.y//分析3
}
override fun onDraw(canvas: Canvas) {
canvas.drawBitmap(mBitmap, mCanvasOffsetPoint.x, mCanvasOffsetPoint.y, mPaint)
}
}
接下来我们来分析一下:
- 分析1:当新手指按下时,我们要设置当前监控的手指为这个新手指
- 分析2:当抬起当前监控的手指时(非当前监控的手指抬起不用做任何处理),我们要切换当前监控的手指为最后一个按下的手指。此时要分两种情况,即当前抬起的手指是否是最后一个按下的手指。我们画图分析一下:
假如当前抬起的手指是最后一个按下的手指(即index为4的手指),那么抬起后应切换当前监控的 index 为 pointerCount - 2 (即index为3的手指) 。如果不是最后一个按下的手指(假如是index为3的手指),那么抬起后应切换当前监控的 index 为 pointerCount - 1(即 index 为4的手指)。
- 分析3:切换监控的手指时,要记录当前的偏移值,避免切换后跳动。
三、配合型 / 协作型
配合型 / 协作型即所有触摸到 View 的 pointer 共同起作用,比如可以多指滑动列表,滑动的距离即多指的焦点(中点)
/**
* 多指触控:协作型
* 忽略个体,只看整体的焦点
*/
class MultiTouchView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
companion object {
val IMAGE_WIDTH = Utils.dp2px(200)
}
private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var mBitmap: Bitmap
//手指按下时的坐标
private var mDownPoint = PointF()
//canvas偏移坐标
private var mCanvasOffsetPoint = PointF()
//canvas上一次偏移坐标
private var mCanvasLastOffsetPoint = PointF()
//所有pointer的焦点(中心点)
private var mPointerFocusPoint = PointF()
//pointer数量
private var mPointerCount = 0
init {
mBitmap = Utils.decodeBitmap(resources, R.drawable.avatar, IMAGE_WIDTH.toInt())
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
//pointer数量
mPointerCount = event.pointerCount
//所有pointer的x、y总和
var sumX = 0f
var sumY = 0f
//是否是pointer_up事件
val isPointerUp = event.actionMasked == MotionEvent.ACTION_POINTER_UP
for (i in 0 until mPointerCount) {
//抬起的那个pointer不用计算
if (!(isPointerUp && i == event.actionIndex)) {
sumX += event.getX(i)
sumY += event.getY(i)
}
}
if (isPointerUp) {
//如果是pointer_up抬起事件,则pointer总数量-1
mPointerCount -= 1
}
//计算焦点
mPointerFocusPoint.x = sumX / mPointerCount
mPointerFocusPoint.y = sumY / mPointerCount
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN,
MotionEvent.ACTION_POINTER_UP -> {
mDownPoint.x = mPointerFocusPoint.x
mDownPoint.y = mPointerFocusPoint.y
mCanvasLastOffsetPoint.x = mCanvasOffsetPoint.x
mCanvasLastOffsetPoint.y = mCanvasOffsetPoint.y
}
MotionEvent.ACTION_MOVE -> {
mCanvasOffsetPoint.x = mCanvasLastOffsetPoint.x + mPointerFocusPoint.x - mDownPoint.x
mCanvasOffsetPoint.y = mCanvasLastOffsetPoint.y + mPointerFocusPoint.y - mDownPoint.y
invalidate()
}
}
return true
}
override fun onDraw(canvas: Canvas) {
canvas.drawBitmap(mBitmap, mCanvasOffsetPoint.x, mCanvasOffsetPoint.y, mPaint)
}
}
四、各自为战型
主要的应用场景是绘图应用,多个手指可以同时个不干扰的绘制,实现也较简单,我们直接看下代码就能明白:
/**
* 多指触控:各自为战型
*/
class MultiTouchView3(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var mPaths = SparseArray<Path>()
init {
mPaint.style = Paint.Style.STROKE
mPaint.strokeWidth = Utils.dp2px(4)
mPaint.strokeCap = Paint.Cap.ROUND
mPaint.strokeJoin = Paint.Join.ROUND
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> {
val actionIndex = event.actionIndex
val pointerId = event.getPointerId(actionIndex)
val path = Path()
path.moveTo(event.getX(actionIndex), event.getY(actionIndex))
mPaths.append(pointerId, path)
}
MotionEvent.ACTION_MOVE -> {
for (i in 0 until event.pointerCount) {
val path = mPaths.get(event.getPointerId(i))
path.lineTo(event.getX(i), event.getY(i))
}
invalidate()
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
//抬起手指时删除绘制
val pointerId = event.getPointerId(event.actionIndex)
mPaths.remove(pointerId)
invalidate()
}
}
return true
}
override fun onDraw(canvas: Canvas) {
for (i in 0 until mPaths.size()) {
canvas.drawPath(mPaths.valueAt(i), mPaint)
}
}
}