Android中的触摸事件——MotionEvent中的多点触控
前言
触摸事件在Android手机中有多重要不言而喻,用户的每次操作都和它有关。不知道大家有没有见过一些自定义的控件有这样的问题:当单手操作的时候,没有问题,但是当第二根手指头放上去的时候,该View中的内容就会“跳动一下”,如果有这样问题的控件,就是没有处理多点触控。该篇内容主要讲解MotionEvent对象中的多点触控信息,以及RecyclerView对于这种多点触控是如何处理的。MotionEvent又是啥呢?用来记录用户在屏幕中的触摸信息的对象。假如你点了一下屏幕(假如你手没抖,而且速度很快),这个时候就会产生两个MotionEvent对象(UP和DOWM)。
正文
一次完整的触摸事件流(官方叫gesture)至少包括ACTION_DOWN和ACTION_UP两个Event(手指触摸的情况,不考虑使用鼠标的情况,本文后面所描述的所有情况都是手指触摸的情况)。前面提到了Event的ACTION,我们一般有两个方法可以取到ACTION,一个是getAction()
,还有一个就是getActionMasked()
。这两个方法有什么区别呢?action中包含了Pointer的Index,而actionMasked中不包含这个Index。那什么又是Pointer呢?这就涉及到该篇内容要重点讨论的多点触控。可以理解成每一个触控点,通俗点讲就是你放在屏幕上的手指头。我们一般都用ActionMasked这个方法来拿这些Action。下面就简单来说明一下这些常见的Event。
- ACTION_DOWN
第一根手指头触摸到屏幕(之前屏幕上没有手指头),一次事件触摸流的开始,很简单,但是很重要,这里也要简单的提一下,在ViewGroup中也是根据这次事件的坐标来决定该次事件流交给谁来处理,直到这次事件流完成(ACTION_UP)。
- ACTION_MOVE
就是你的手指头在屏幕上滑动,就会产生这个事件。
- ACTION_UP
最后一根手指头离开屏幕(屏幕上没有手指头了),标志着该次事件流已经完成。
- ACTION_CANCEL
这次事件流被取消了,虽然还没有完成,一般是ViewGroup经过某种条件判断会设置这样的ACTION。
- ACTION_POINTER_DOWN
当屏幕上已经有手指头的时候,再按一个手指头下去就会触发这个事件。
- ACTION_POINTER_UP
当手指头离开屏幕,同时屏幕上还有手指头的时候就会触发这个事件。
如果你的自定义控件处理好了上面的6种ACTION,那么你的控件对触摸的处理就很好了,因为RecyclerView就只是处理了这6种Event。
在一个MotionEvent对象中,包含了你在屏幕上所有的触摸点信息,他默认会有一个类似于active的触摸点,可以通过方法getActionIndex()
拿到这个触摸点的Index,然后再通过方法getPointerId()
能拿到这个触摸点的Id,Id通过findPointerIndex()
,能再拿到这个Index。这里需要注意的是在一次事件流中,同一个触摸点的Index是可能发生改变的,但是Id是不会改变的。在方法getX()
和getY()
中都可以传一个Index来拿你想要的触摸点的坐标,不止这两个方法可以传入index,其他的读者自己去研究了。下面我们来讨论下不同情况下active的默认触摸点都是哪些点呢?
- ACTION_DOWN, ACTION_MOVE, ACTION_UP
这些ACTIONs默认的active触摸点Index都是0,也就是说这些事件如果你的初次点击屏幕的手指头没有离开屏幕,那就一直是这个点,如果这个手指头已经离开屏幕,那这个点就变成了第二个点击屏幕的点,依次类推。
- ACTION_POINTER_DOWN
默认active触摸点是新点击到屏幕上的那个点,但是在后续的move中这个默认index又回变成0。这里也可以简单解释下文章开篇中提到的“跳一下”的Bug:因为在第二个手指头点击屏幕的瞬间,active的触摸点为第二个手指头,这个时候默认的坐标也是第二个指头,后续的move事件中默认的active触摸点又会变成第一个手指头,所以会出现跳一下的Bug。
- ACTION_POINTER_UP
这个Event和ACTION_POINTER_DOWN类似,只是默认的active的Index变成了离开屏幕的那个触摸点。
下面我们来看看RecyclerView中是如何来处理多点触控的。
@Override
public boolean onTouchEvent(MotionEvent e) {
final MotionEvent vtev = MotionEvent.obtain(e);
final int action = e.getActionMasked();
// 默认active的Index
final int actionIndex = e.getActionIndex();
switch (action) {
case MotionEvent.ACTION_DOWN: {
// 直接取第一个Pointer为自己要处理的,将它的Id保存到成员变量中
mScrollPointerId = e.getPointerId(0);
// 记录初始化的坐标
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
} break;
case MotionEvent.ACTION_POINTER_DOWN: {
// 当第二个手指头(也可能是第n个手指头)点击屏幕的时候,就会用这个手指头来接管这次事件流。
mScrollPointerId = e.getPointerId(actionIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
} break;
case MotionEvent.ACTION_MOVE: {
// 通过pointerID拿到需要处理的Index。
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id "
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
// 获取需要处理的点的坐标。
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
} break;
case MotionEvent.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
case MotionEvent.ACTION_UP: {
resetTouch();
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
} break;
}
}
// 处理pointer up的情况。
private void onPointerUp(MotionEvent e) {
// 离开屏幕的触摸点index
final int actionIndex = e.getActionIndex();
// 如果离开的那个点的id正好是我们接管触摸的那个那个点,那么我们就需要重新再找一个pointer来接管,反之不用管。
if (e.getPointerId(actionIndex) == mScrollPointerId) {
// Pick a new pointer to pick up the slack.
// 如果离开屏幕的点index是0,那就用index 为 1 的点,反之就直接用0。
final int newIndex = actionIndex == 0 ? 1 : 0;
// 重置id和初始化initX和initY。
mScrollPointerId = e.getPointerId(newIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
}
}
这里简单说明下:一个手指到滑动就不说了,当屏幕上有新的手指加入时(之前屏幕上已经有手指头了),这个新加入的手指就会接管RecyclerView的滑动。当有手指头离开屏幕时(屏幕上还有其他的手指头),这个时候如果离开的手指头刚好时接管滑动的那个手指头,这个时候就会找index为0或着1的手指头重新接管滑动;如果离开的不是接管滑动的手指头,就不用管。
到这里多点触控相关的内容就没了,如果有错误的地方,欢迎大家指出来。后面可能还会有触摸事件的发送和滚动相关内容。