文章目录
事件拦截机制
阅读下面的多点触控原理知识,需要了解一定的事件拦截机制原理,可以阅读文章:Android自定义View系列:事件拦截机制原理
getAction() 和 getActionMasked() 的区别
现在网上还是有很多博客在 onTouchEvent()
处理触摸反馈判断时使用的 MotionEvent.getAction()
,那么 MotionEvent.getAction()
和 MotionEvent.getActionMasked()
有什么区别呢?为什么推荐我们使用 MotionEvent.getActionMasked()
?
MotionEvent.getAction()
是在早期 Android 版本就已经存在,在只有单点触控的时候 只包含事件信息,比如 MotionEvent.ACTION_DOWN 、MotionEvent.ACTION_UP、MotionEvent.ACTION_MOVE。
但在多点触控时它就多了一个信息:还需要知道按下的时候你是第一个手指还是非第一个手指,抬起的时候是最后一个手指抬起了还是非最后一个手指抬起了。
因为多点触控的处理逻辑和单点触控不同,比如 MotionEvent.ACTION_POINTER_DOWN、MotionEvent.ACTION_POINTER_UP,所以就要区分两种分别处理,Android 因此在 API 8 提供了 MotionEvent.getActionMasked()
和 MotionEvent.getActionIndex()
。
那么 MotionEvent.getAction()
就要将两个信息压缩到32位的 int 类型里面,而 MotionEvent.getActionMasked()
是拆分了信息只有事件信息。
因此在现在的单点触控情况下使用 MotionEvent.getAction()
是可以正常使用的,但在多点触控情况下,它没有将多点触控的信息拆分全部压在32位 int 类型里面,所以处理的信息就会是错误的。
实际开发中,我们推荐使用 MotionEvent.getActionMasked()
和 MotionEvent.getActionIndex()
来分别判断回调的事件信息和获取哪个手指处理事件。
单点触控事件序列:
ACTION_DOWN P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_UP P(x, y)
多点触控事件序列:
ACTION_DOWN:第一个手指按下(之前没有任何手指触摸到View)
ACTION_MOVE:有手指发生移动
ACTION_MOVE
ACTION_POINTER_DOWN:额外手指按下(按下之前已经有别的手指触摸到View)
ACTION_MOVE
ACTION_MOVE
ACTION_POINTER_UP:有手指抬起,但不是最后一个(抬起之后,仍然还有别的手指在触摸着View)
ACTION_MOVE
ACTION_UP:最后一个手指抬起(抬起之后没有任何手指触摸到View,这个手指未必是ACTION_DOWN的那个手指)
可以发现相比单点触控,处理多点触控时多了 MotionEvent.ACTION_POINTER_DOWN 和 MotionEvent.ACTION_POINTER_UP。
多点触控事件序列分析
在说明多点触控的三种使用场景之前,有必要理解多点触控事件序列在处理过程中与单点触控的不同。
index 和 id
上面的多点触控事件序列我们再将详细信息追加上:
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_UP P(x, y, index, id) P(x, y, index, id)
ACTION_MOVE P(x, y, index, id)
ACTION_UP P(x, y, index, id)
pointer 多了一些数据:index 和 id。
- index:当事件序列开始 MotionEvent.ACTION_DOWN 的时候,该手指默认会分配一个索引 index=0,表示在多点触控时该手指的索引,通过该索引我们可以在
onTouchEvent()
时通过MotionEvent.getX(index)
或MotionEvent.getY(index)
获取手指在 View 的坐标
需要特别注意的是:index 它并不是固定的,它根据事件序列的改变而改变,它总是有序的。
比如:
ACTION_DOWN P(x, y, 0, 0)
ACTION_MOVE P(x, y, 0, 0)
ACTION_MOVE P(x, y, 0, 0)
ACTION_POINTER_DOWN P(x, y, 0, 0) P(x, y, 1, 1)
ACTION_MOVE P(x, y, 0, 0) P(x, y, 1, 1)
ACTION_MOVE P(x, y, 0, 0) P(x, y, 1, 1)
ACTION_POINTER_UP P(x, y, 0, 0) P(x, y, 1, 1)
ACTION_MOVE P(x, y, 0, 1)
ACTION_UP P(x, y, 0, 1)
上面的事件序列假设有两个手指,触发 MotionEvent.ACTION_DOWN 手指为 index=0,id=0。
在事件处理过程中 MotionEvent.ACTION_POINTER_DOWN 被回调,说明另一个手指加入事件序列,手指为 index=1, id=1。
在事件处理过程中 MotionEvent.ACTION_POINTER_UP 被回调,有手指松开,松开的手指是最开始触摸 View 的第一个手指 index=0, id=0,所以在最后是手指 id=1 的手指继续事件序列,并且它的 index 变为 0。
我们可以看下 getX()
和 getY()
的源码:
public final float getX() {
return nativeGetAxisValue(mNativePtr, AXIS_X, 0, HISTORY_CURRENT);
}
public final float getY() {
return nativeGetAxisValue(mNativePtr, AXIS_Y, 0, HISTORY_CURRENT);
}
getX()
和 getY()
的源码其中 pointerIndex(第三个参数) 的值默认为 0,就是上面举例的index。
所以在多点触控中,getX()
和 getY()
是不可用的,它已经将 pointerIndex 写死值为 0,所以无法准确获取到需要手指的坐标位置。需要更改为使用 getX(pointerIndex)
和 getY(pointerIndex)
来获取:
public final float getX(int pointerIndex) {
return nativeGetAxisValue(mNativePtr, AXIS_X, pointerIndex, HISTORY_CURRENT);
}
public final float getY(int pointerIndex) {
return nativeGetAxisValue(mNativePtr, AXIS_Y, pointerIndex, HISTORY_CURRENT);
}
既然 index 是随时都在变化的,那么你可能会疑问:那在多点触控的时候,对应手指的索引要怎么拿?我应该怎么才能拿到触摸 View 手指的坐标?
答:id。
上面事件序列的 pointer 还有 id,id 在整个事件序列中它都是固定不变的,通过它我们就可以获取手指当前的 index,继而通过 index 再调用 getX(index)
和 getY(index)
获取到坐标点。
Android 给我们提供了几个 API 在多点触控场景下使用:
-
MotionEvent.getX(pointerIndex):通过 pointerIndex 获取触摸 View 的某个手指的横坐标
-
MotionEvent.getY(pointerIndex):通过 pointerIndex 获取触摸 View 的某个手指的纵坐标
-
MotionEvent.getPointerId(pointerIndex):通过 pointerIndex 获取触摸 View 的某个手指 id
-
MotionEvent.findPointerIndex(id):通过 id 获取触摸View的某个手指当前的 pointerIndex 索引
-
MotionEvent.getActionIndex():在 MotionEvent.ACTION_POINTER_DOWN 获取非第一个手指按下的手指索引,在 MotionEvent.ACTION_POINTER_UP 获取非最后一个手指按下的索引。该 API 在多点触控只适用于上面两种常见回调使用
getActionIndex()
getActionIndex()
这个 API 比较特殊,它在多点触控的场景只适用于 MotionEvent.ACTION_POINTER_DOWN 和 MotionEvent.ACTION_POINTER_UP 事件回调时使用。那为什么不能在 MotionEvent.ACTION_MOVE 获取调用?
当多个手指触摸的时候,其实多个手指是在同时移动的(手指的轻微移动都会回调 MotionEvent.ACTION_MOVE)。
我们上面说到 pointer 的索引 index 是会根据事件序列的改变而改变的,也就是说导致 MotionEvent.ACTION_MOVE 的手指索引是不断切换的。
所以在 MotionEvent.ACTION_MOVE 并不方便获取正在导致那个事件的手指,也是没有意义的,在 MotionEvent.ACTION_MOVE 调用 MotionEvent.getActionIndex()
总是返回 0,该值没有任何意义不代表哪个手指的索引。
多点触控的三种使用场景
接力型
接力型:同一时刻只有一个 pointer 起作用,即最新的 pointer。典型:ListView、RecyclerView。
接力型的场景说明简单来说就是:假设有两个手指,一个手指在触摸移动 View 的时候,另一个手指介入此次事件序列,然后其中一个手指松手,另一个手指接管了事件序列直到结束。
实现方式:在 MotionEvent.ACTION_POINTER_DOWN 和 MotionEvent.ACTION_POINTER_UP 时记录下最新的 pointer,在之后的 MotionEvent.ACTION_MOVE 事件中使用这个 pointer 来判断位置。
demo:多点触控接力移动图片
public class MultiTouchView extends View {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Bitmap bitmap;
private float downX;
private float downY;
private float originalOffsetX;
private float originalOffsetY;
private float offsetX;
private float offsetY;
private int trackingPointerId;
public MultiTouchView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.picture);
}
// 多指接力型(两个手指滑动时,一个手指放开另外一个手指接手滑动)
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: // 第一根手指按下回调
trackingPointerId = event.getPointerId(0); // 第一根手指index为0
updateOffset(event, 0);
break;
case MotionEvent.ACTION_MOVE:
int index = event.findPointerIndex(trackingPointerId); // 根据id获取索引
offsetX = event.getX(index) - downX + originalOffsetX;
offsetY = event.getY(index) - downY + originalOffsetY;
invalidate();
break;
case MotionEvent.ACTION_POINTER_DOWN: // 非第一根手指按下回调
// 有新手指加入事件序列,更新跟踪的手指id
int actionIndex = event.getActionIndex();
trackingPointerId = event.getPointerId(actionIndex);
// 更新为新手指的坐标,让新手指接管事件
updateOffset(event, actionIndex);
break;
case MotionEvent.ACTION_POINTER_UP: // 非第一个手指松手回调
actionIndex = event.getActionIndex();
int pointerId = event.getPointerId(actionIndex);
// 松手的手指是当前正在跟踪的手指
if (pointerId == trackingPointerId) {
int newIndex;
// getPointerCount()此时还是包含松手的手指数算在内
if (actionIndex == event.getPointerCount() - 1) {
newIndex = event.getPointerCount() - 2;
} else {
newIndex = event.getPointerCount() - 1;
}
trackingPointerId = event.getPointerId(newIndex);
updateOffset(event, newIndex);
}
break;
}
return true;
}
private void updateOffset(MotionEvent event, int index) {
downX = event.getX(index);
downY = event.getY(index);
originalOffsetX = offsetX;
originalOffsetY = offsetY;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmap, offsetX, offsetY, paint);
}
}
配合型/协作型
配合型/协作型:所有触摸到 View 的 pointer 共同起作用。典型:ScaleGestureDetector、GestureDetector 的 onScroll() 判断。
配合型/协作型的场景说明简单来说就是:假设有多个手指触摸 View,选择多个手指的中心位置作为焦点坐标处理 View。
实现方式:在每个 MotionEvent.MotionEvent.ACTION_DOWN、MotionEvent.ACTION_POINTER_DOWN、MotionEvent.ACTION_POINTER_UP、MotionEvent.ACTION_UP 事件中使用所有 pointer 的坐标来共同更新焦点坐标,并在 MotionEvent.ACTION_MOVE 中使用所有 pointer 的坐标来判断位置。
demo:多个手指触摸 View 协同移动图片
public class MultiTouchView extends View {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Bitmap bitmap;
private float downX;
private float downY;
private float originalOffsetX;
private float originalOffsetY;
private float offsetX;
private float offsetY;
public MultiTouchView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.picture);
}
// 多指协作型(通过多个手指的横纵坐标计算出她们的中心点坐标,以中心点坐标作为滑动坐标)
@Override
public boolean onTouchEvent(MotionEvent event) {
float sumX = 0;
float sumY = 0;
// 多指滑动松开时,因为ACTION_POINTER_UP此时还在进行中,所以获取的event.getPointerCount()数量是不对的
// 比如现在两个手指在滑动bitmap,现在松开一个手指,event.getPointerCount()还是2
// 如果不过滤ACTION_POINTER_UP松开的手指数量就会导致计算错误,出现bitmap位置跳一下到当前手指触摸位置的问题
boolean isPointerUp = event.getActionMasked() == MotionEvent.ACTION_POINTER_UP;
for (int i = 0; i < event.getPointerCount(); i++) {
// 不是抬起事件,计算没有抬起的手指的横纵坐标
if (!isPointerUp || i != event.getActionIndex()) {
sumX += event.getX(i);
sumY += event.getY(i);
}
}
int pointerCount = event.getPointerCount();
if (isPointerUp) pointerCount--; // 将抬起的手指的数量去掉
// 计算多个手指的协同的中心位置,即用多个手指的横纵坐标的和 / 点数量 求出平均中心点坐标
// 用中心点坐标作为bitmap的移动位置
float focusX = sumX / pointerCount;
float focusY = sumY / pointerCount;
// 因为现在只要关注多指触摸计算的中心点坐标,所以按下、多指按下、多指松开的操作都是重新计算中心点坐标
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_POINTER_UP:
downX = focusX;
downY = focusY;
originalOffsetX = offsetX;
originalOffsetY = offsetY;
break;
case MotionEvent.ACTION_MOVE:
offsetX = focusX - downX + originalOffsetX;
offsetY = focusY - downY + originalOffsetY;
invalidate();
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmap, offsetX, offsetY, paint);
}
}
各自为战型
各自为战型:各个 pointer 做不同的事,互不影响。典型:支持多画笔的画板应用。
实现方式:在每个 MotionEvent.ACTION_DOWN、MotionEvent.ACTION_POINTER_DOWN 中记录下每个 pointe r的 id,在 MotionEvent.ACTION_MOVE 中使用 id 对它们进行跟踪。
demo:多指触摸 View 实现画板
public class MultiTouchView extends View {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private SparseArray<Path> paths = new SparseArray<>();
public MultiTouchView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
{
paint.setStrokeWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics()));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
}
// 多指画板
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
int actionIndex = event.getActionIndex();
int pointerId = event.getPointerId(actionIndex);
Path path = new Path();
path.moveTo(event.getX(actionIndex), event.getY(actionIndex));
paths.append(pointerId, path);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
for (int i = 0; i < event.getPointerCount(); i++) {
path = paths.get(i);
if (path != null) {
path.lineTo(event.getX(i), event.getY(i));
}
}
invalidate();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
actionIndex = event.getActionIndex();
pointerId = event.getPointerId(actionIndex);
paths.remove(pointerId);
invalidate();
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < paths.size(); i++) {
Path path = paths.valueAt(i);
if (path != null) {
canvas.drawPath(path, paint);
}
}
}
}
结合多点触控的触摸事件结构总结
-
触摸事件是按序列来分组的,每一组事件必然以 MotionEvent.ACTION_DOWN 开头,以 MotionEvent.ACTION_UP 或 MotionEvent.ACTION_CANCEL 结束
-
MotionEvent.ACTION_POINTER_DOWN 和 MotionEvent.ACTION_POINTER_UP 和 MotionEvent.ACTION_MOVE 一样,只是事件序列中的组成部分,并不会单独分出新的事件序列
-
触摸事件序列是针对 View 的,而不是针对 pointer 的。【某个 pointer 的事件】这种说法是不正确的
-
在一个触摸事件里,每个 pointer 除了 x 和 y 之外,还有 index 和 id
-
【移动的那个手指】这个概念是伪概念,【寻找移动的那个手指】这个需求是个伪需求