赞
踩
阅读下面的多点触控原理知识,需要了解一定的事件拦截机制原理,可以阅读文章:Android自定义View系列:事件拦截机制原理
现在网上还是有很多博客在 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。
在说明多点触控的三种使用场景之前,有必要理解多点触控事件序列在处理过程中与单点触控的不同。
上面的多点触控事件序列我们再将详细信息追加上:
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。
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()
这个 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
【移动的那个手指】这个概念是伪概念,【寻找移动的那个手指】这个需求是个伪需求
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。