触摸事件【MotionEvent】简介

MotionEvent简介


当用户触摸屏幕时,将创建一个MontionEvent对象,MotionEvent包含了关于发生触摸的位置、时间信息,以及触摸事件的其他很多细节。


Android 将所有的输入事件都放在了 MotionEvent 中,随着安卓的不断发展壮大,MotionEvent 也开始变得越来越复杂,下面是网上整理的 MotionEvent几次比较大的变动:
  • Android 1.0 (API 1 )    支持单点触控和轨迹球的事件。
  • Android 1.6 (API 4 )    支持手势。
  • Android 2.0 (API 5 )    支持多点触控。
  • Android 3.1 (API 12)   支持触控笔,鼠标,键盘,操纵杆,游戏控制器等输入工具。

获取MontionEvent对象的方式主要有以下两种:
  • 在View或Activity的onTouchEvent方法中
  • 实现OnTouchListener接口后在onTouch方法中

MotionEvent官方文档翻译

MontionEvent描述了动作的动作代码和一些列的坐标值。动作代码表明了当触点按下或者弹起等引起的状态变化。坐标值描述了位置信息以及以他的运动属性。
例如,当用户第一次触摸屏幕的时候,系统给窗体发出一个触摸事件,动作代码为ACTION_DOWN,并提供了一些列的坐标值,比如触摸的X、Y坐标,接触区域的压力、尺寸、方向等信息。
一些设备能够在同一时间报告多条运动轨迹。多点触控屏幕为每个手指都发出一条运动轨迹。手指或者其他能够产生运动轨迹的物体都可以叫做触点。运动事件包含所有触点的信息,即使有些触点自从上次事件之后就没有再移动,这些触点必须是当前处于活动状态的。
只有触点按下或者抬起的时候才会影响触点的数量,动作取消的时候除外。
每个触点都有一个唯一的id,这个id是在触点第一次按下的时候(动作代码为ACTION_DOWN或者ACTION_POINTER_DOWN)由系统自动分配的。触点的id会一直保持有效,当触点抬起的时候(动作代码为ACTION_UP或者ACTION_POINTER_UP)或者动作取消(动作代码为ACTION_CANCEL)的时候会导致触点的id失效。
MotionEvent类提供了许多可以查看触点的位置或者其他信息的方式,比如getX(int)、getY(int)、getAxisValue(int)、getPointerId(int)、getToolType(int)。这其中的大部分方法都将触点的索引值作为参数而不是触点的id。在事件中,每个触点的索引号的取值范围是从0到getPointerCount()-1。
在一次运动中触点出现的顺序是不确定的。因此,触点的索引值会由于事件的变化而变化,但是只要触点处于活动状态,该触点的id就不会改变。用getPointerId(int)方法可以得到触点的id值,从而根据得到的id值在一连串的动作中来追踪其运动轨迹。然而在连续的运动事件中,应该用findPointerIndex(int)方法通过触点的id值得到触点的索引值。

历史数据 批处理 Batching

由于Android设备对于触摸事件的反应非常灵敏,手指稍微移动一下就会产生一个移动事件,所以移动事件会产生的特别频繁。为了提高效率,系统会将近期的多个移动事件按照事件发生的顺序进行排序打包后放在同一个 MotionEvent 中,与之对应的产生了以下方法:
  • getHistorySize()  获取历史事件集合大小
  • getHistoricalX(int pos)  获取第pos个历史事件x坐标(pos < getHistorySize())
  • getHistoricalX (int pin, int pos)  获取第pin个手指的第pos个历史事件x坐标(pin < getPointerCount(), pos < getHistorySize())

注意:
  • pin全称是pointerIndex,表示第几个手指,此处为了节省空间使用了缩写。
  • 历史数据只有 ACTION_MOVE 事件。
  • 历史数据单点触控和多点触控均可以用。

官方文档翻译:
为了提高效率,Android系统在处理ACTION_MOVE事件时可能会将连续的几个多触点移动事件打包到一个MotionEvent对象中,我们可以通过getX(int)和getY(int)来获得最近(当前)发生的一个触摸点事件的坐标,或使用getHistorical(int,int)和getHistorical(int,int)来获得时间稍早的触点事件的历史坐标,这些坐标之所以被成为"历史坐标",是因为这些坐标比当前坐标更早的出现了。要想按照时间顺序处理所有坐标,首先要处理通过getHistoricalXX相关函数获得的事件信息,然后再处理当前的事件信息。

官方文档中例子:
void printSamples(MotionEvent ev) {
final int historySize = ev.getHistorySize();
final int pointerCount = ev.getPointerCount();
for (int h = 0; h < historySize; h++) {
System.out.printf("At time %d:", ev.getHistoricalEventTime(h));
for (int p = 0; p < pointerCount; p++) {
System.out.printf(" pointer %d: (%f,%f)", ev.getPointerId(p), ev.getHistoricalX(p, h), ev.getHistoricalY(p, h));
}
}
System.out.printf("At time %d:", ev.getEventTime());
for (int p = 0; p < pointerCount; p++) {
System.out.printf(" pointer %d: (%f,%f)", ev.getPointerId(p), ev.getX(p), ev.getY(p));
}
}


【常用API】

MotionEvent中的方法基本都是引用的native方法。
最常用
  • getAction()  获取事件类型,注意多点触控时获取到的是由pointer的index值和事件类型值组合而成的值
  • getActionMasked() 与getAction()类似,多点触控时必须使用这个方法获取事件类型
  • getActionIndex()  获取该事件是哪个指针(手指)产生的
  • getPointerCount()  获取在屏幕上手指的个数
  • getPointerId(int pointerIndex)  获取一个pointer的唯一标识符ID,在手指按下和抬起之间ID始终不变
  • findPointerIndex(int pointerId)  通过PointerId获取到当前状态下PointIndex,之后便可以通过PointIndex获取其他内容
获取事件发生的位置坐标
  • getX()  获取第1个触摸点触摸的中间区域相对当前 View 的 X 轴坐标
  • getRawX()  和上面getX()不同的是,此方法获得的是相对屏幕的原始的位置的 X 轴坐标
  • getX(int pointerIndex)  获取第pointerIndex个触控点触摸的中间区域相对当前 View 的 X 轴坐标
获取事件发生的时间
  • getDownTime()  获取手指按下时的时间。返回值类型为 long,单位是毫秒
  • getEventTime()  获取当前事件发生的时间。返回值类型为 long,单位是毫秒
获取压力和接触面积的大小
  • getSize()  获取第1个手指与屏幕接触面积的大小
  • getSize(int pin)  获取第pin个手指与屏幕接触面积的大小
  • getPressure()  获取第一个手指的压力大小
  • getPressure(int pin)   获取第pin个手指的压力大小
历史事件
  • getHistorySize()  获取历史事件集合大小
  • getHistoricalX(int pos)  获取第1个手指的第pos个历史事件x坐标(pos < getHistorySize())
  • getHistoricalX (int pin, int pos)  获取第pin个手指的第pos个历史事件x坐标(pin < getPointerCount())
  • getHistoricalEventTime(int pos)  获取历史事件发生的时间。返回值类型为 long,单位是毫秒
  • getHistoricalSize(int pos)  获取历史数据中第1个手指在第pos次事件中的接触面积
  • getHistoricalSize(int pin, int pos)  获取历史数据中第pin个手指在第pos次事件中的接触面积
  • getHistoricalPressure(int pos)   获取历史数据中第1个手指在第pos次事件中的压力大小
  • getHistoricalPressure(int pin, int pos)  获取历史数据中第pin个手指在第pos次事件中的压力大小
其他
  • getToolType(int pin)  获取对应的输入设备类型
  • getEventTime()-event.getDownTime())  总共按下时花费时间
  • getEdgeFlags()  当事件类型是ActionDown时可以通过此方法获得手指触控开始的边界,有如下几种值:EDGE_LEFT、EDGE_TOP、EDGE_RIGHT、EDGE_BOTTOM

注意:
  • 获取接触面积大小和获取压力大小是需要硬件支持的。
  • 非常不幸的是大部分设备所使用的电容屏不支持压力检测,但能够大致检测出接触面积。
  • 大部分设备的 getPressure() 是使用接触面积来模拟的。
  • 由于某些未知的原因(可能系统版本和硬件问题),某些设备不支持该方法。
结论:由于获取接触面积和获取压力大小受系统和硬件影响,使用的时候一定要进行数据检测,以防因为设备问题而导致程序出错。

单点触控的动作事件

ACTION_DOWN==0:表示用户开始触摸(在第一个触摸点按下时触发)。
这个是TouchEvent事件的开始,任何事件都必须手指按下去才行。
A pressed gesture has started, the motion contains the initial starting location. This is also a good time to check the button state to distinguish分辨 secondary and tertiary button clicks and handle them appropriately适当的. Use {@link #getButtonState} to retrieve the button state.

ACTION_UP==1:表示用户抬起了手指(当屏幕上唯一的点被放开时触发)。
正常情况下,表示手指离开了屏幕。
A pressed gesture has finished, the motion contains the final release location as well as此外 any intermediate中间的 points since the last down or move event.

ACTION_MOVE==2:表示触摸点在移动(当有点在屏幕上移动时一直触发)。
注意的是,由于灵敏度很高,所以基本上只要有触摸点在屏幕上(即使"不动"),此事件就会不停地被触发。
A change has happened during a press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).The motion contains the most recent point, as well as any intermediate points since the last down or move event.

ACTION_CANCEL==3:表示手势被取消了(事件被上层拦截了,由父View发送,不是用户自己触发的)。
事实上,只有上层 View 回收事件处理权的时候,ChildView 才会收到一个 ACTION_CANCEL 事件。
例如:上层 View 是一个 RecyclerView,它收到了一个 ACTION_DOWN 事件,由于这个可能是点击事件,所以它先传递给对应 ItemView,询问 ItemView 是否需要这个事件,然而接下来又传递过来了一个 ACTION_MOVE 事件,且移动的方向和 RecyclerView 的可滑动方向一致,所以 RecyclerView 判断这个事件是滚动事件,于是要收回事件处理权,这时候对应的 ItemView 就会收到一个 ACTION_CANCEL ,并且不会再收到后续任何事件。
The current gesture has been aborted. You will not receive any more points in it.  You should treat this as an up event, but not perform any action that you normally would.

ACTION_OUTSIDE==4:表示用户触碰超出了正常的UI边界(这是个奇葩)。
ACTION_OUTSIDE的触发条件更加奇葩!从字面上看,outside 意思不就是超出区域么?然而不论你如何滑动超出控件区域都不会触发 ACTION_OUTSIDE 这个事件。相信很多魔法师都对此很是疑惑,说好的超出区域呢?
我们知道,正常情况下,如果初始点击位置在该视图区域之外,该视图根本不可能会收到事件。然而,这并不是绝对的,你可曾还记得点击 Dialog 区域外关闭吗?Dialog 就是一个特殊的视图(一个没有占满屏幕大小的窗口),能够接收到视图区域外的事件。当然啦,想要像 Dialog 一样可以接收到视图之外的事件需要一些特殊的设置:设置视图的 WindowManager 布局参数的 flags为FLAG_WATCH_OUTSIDE_TOUCH。这样当点击事件发生在这个视图之外时,该视图就可以接收到一个 ACTION_OUTSIDE 事件了。
由于这个事件用到的几率比较小,此处就不展开叙述了,以后用到的时候再详细讲解。
A movement has happened outside of the normal bounds of the UI element. This does not provide a full gesture, but only the initial location of the movement/touch 不再提供完整的手势,只提供 运动/触摸 的初始位置.

多点触控的动作事件

ACTION_POINTER_DOWN==5:当屏幕上已经有一个点被按住,此时再按下其他点时触发。


ACTION_POINTER_UP==6:在多个触摸点存在的情况下,其中一个触摸点消失了。此时,还有触摸点存在,也就是说用户还有手指触摸屏幕。相对而言,ACTION_UP可以说是最后一个触摸点消失时产生的。 

如果用户【先两个手指先后接触屏幕,然后同时滑动,最后再先后离开】,这一套动作所产生的事件流是什么样的呢?
  • 先产生一个ACTION_DOWN事件,代表用户的第一个手指接触到了屏幕。
  • 再产生一个ACTION_POINTER_DOWN事件,代表用户的第二个手指接触到了屏幕。
  • 然后会产生很多的ACTION_MOVE事件,但是在这些MotionEvent对象中,都保存着两个触摸点滑动的信息。
  • 再然后产生一个ACTION_POINTER_UP事件,代表用户的一个手指离开了屏幕。
  • 此时如果剩下的那个手指还在滑动时,仍会产生很多ACTION_MOVE事件。
  • 最后产生一个ACTION_UP事件,代表用户的最后一个手指离开了屏幕

多点触控的掩码常量

  • ACTION_MASK==0x00ff:动作掩码常量。Bit位 mask屏蔽、掩码 of the parts of the action code that are the action itself.
  • ACTION_POINTER_INDEX_MASK==0xff00:索引掩码常量。Bits in the action code that represent代表 a pointer index, used with {@link #ACTION_POINTER_DOWN} and {@link #ACTION_POINTER_UP}. Shifting移位 down by {@link #ACTION_POINTER_INDEX_SHIFT} provides the actual pointer index where the data for the pointer going up or down can be found; you can get its identifier with {@link #getPointerId(int)} and the actual data with {@link #getX(int)} etc. @see #getActionIndex
  • ACTION_POINTER_INDEX_SHIFT==8:用于右移8位的常量,获取触摸点索引需要移动的位数。Bit shift for the action bits holding the pointer index as defined by {@link #ACTION_POINTER_INDEX_MASK}.

其他动作事件

鼠标
  • ACTION_HOVER_MOVE==7:指针在窗口或者View区域移动,但没有按下。
  • ACTION_SCROLL==8:滚轮滚动,可以触发水平滚动(AXIS_HSCROLL)或者垂直滚动(AXIS_VSCROLL)
  • ACTION_HOVER_ENTER==9:指针移入到窗口或者View区域,但没有按下。
  • ACTION_HOVER_EXIT==10:指针移出到窗口或者View区域,但没有按下。
注意:这些事件类型是 安卓4.0 (API 14) 才添加的,可使用getActionMasked()可以获得这些事件类型。这些事件不会传递到 onTouchEvent(MotionEvent),而是传递到 onGenericMotionEvent(MotionEvent) 。

按钮状态
  • ACTION_BUTTON_PRESS==11:按下按钮
  • ACTION_BUTTON_RELEASE==12:释放按钮

输入设备类型判断

MotionEvent 负责集中处理所有类型设备的输入事件,主要包括以下几种:
  • TOOL_TYPE_ERASER  橡皮擦
  • TOOL_TYPE_FINGER  手指
  • TOOL_TYPE_MOUSE  鼠标,在手机上使用概率也比较小
  • TOOL_TYPE_STYLUS  手写笔、触控笔,处理流程基本相同
  • TOOL_TYPE_UNKNOWN  未知类型
轨迹球(trackball)只出现在最早的设备上,现代的设备上已经见不到了。
可以使用 getToolType(int pointerIndex) 来获取对应的输入设备类型。

单点触控的事件处理

针对单点触控的事件处理一般是这样写的:
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()){
case MotionEvent.ACTION_DOWN:// 手指按下
break;
case MotionEvent.ACTION_MOVE:// 手指移动
break;
case MotionEvent.ACTION_UP:// 手指抬起
break;
case MotionEvent.ACTION_CANCEL:// 事件被拦截
break;
case MotionEvent.ACTION_OUTSIDE:// 超出区域
break;
}
return super.onTouchEvent(event);
}

关于事件类型与索引

可以通过getAction()或getActionMasked()获得事件的类型。getAction获得的int值是由pointer的index值和事件类型值组合而成的,而getActionWithMasked则只返回事件的类型值。
因为android2.0之后一个MotionEvent对象中可以包含多个触摸点的事件,在MotionEvent对象只包含一个触摸点的事件时,上边两个函数的结果是相同的,但是如果包含多个触摸点,二者的结果就不同啦。

1、getAction()和getActionMasked()有什么区别呢?
如果Action的值是在0x00到0xff之间的话,getAction()返回的值和getActionMasked()的返回的值是一样的。
而当mAction的值大于0xff时,二者返回的值是不一样的。

2、什么时候mAction的值会大于0xff呢?
这就是是当有多点触控时。我们知道Android是支持多点触控的,那么我们怎么知道这个MotionEvent是哪一个触控点触发的呢?
这就需要MotionEvent带有触控点索引信息。Android的解决方案是在mAction的第二个8位中存储触控点索引信息。
例如,如果mAction的值是0x0000,则表示是第一个触控点的ACTION_DOWN操作,如果mAction的值是0x0100呢,则表示是第二个触控点的ACTION_DOWN操作。
总而言之,mAction时的低8位(0-7位)是动作类型信息,高8位(8-15位)是触控点的索引信息(即表示是哪一个触控点的事件)。
event.getActionMasked() 和【event.getAction() & MotionEvent.ACTION_MASK】是等价的

3、为什么不用两个字段来表示?
因为动作类型只要0-255(2^8)就可以了,触控的个数也是,两者加在一起只需要16位即可。
一个int类型占4个字节,即4*8=32位,所以完全可以用一个int类型的字段来存储动作类型和触控的个数这两个信息,这样即可以节约内存,又可以提高处理速度。
同样的设计思想也体现在onMeasure中的MeasureSpec上。
不过为了便利(可读性、独立性、解耦),通常我们都是以不同的字段来存储不同的信息的,虽然在内存上浪费了点。

4、如何得索引值呢?
原理:先将action跟0xff00相与清除前8位用于存储动作常量的信息,然后将action右移8位就可以得到索引值了。
即先对action用ACTION_POINTER_INDEX_MASK进行掩码处理,即action & ACTION_POINTER_INDEX_MASK = action & 0xff00
这个掩码也就是将action这个数的前8位清零.
然后再将maskedIndex向右移8位就能够得到索引值了。
android中用于右移8位的常量为:ACTION_POINTER_INDEX_SHIFT

5、为什么要有索引信息?
因为android中,当有触摸事件发生时(假设已经注册了事件监听器),会回调你注册的监听器中的onTouch(MotionEvent ev)方法传递了一个MotionEvent的对象过来。但是,通过上面的方法永远是只传递进来一个MotionEvent过来,如果只是单点触控那是没有问题,但是当你多个手指触控的时候你可能会需要知道每个手指的所对应的触控点的数据信息,所以MotionEvent中就必须要有索引信息。
另外,通过API可以看到,MotionEvent还包含了移动操作中其它历史移动数据,方便处理触控的移动操作。

上一篇:PHP——动态随机数


下一篇:Android 方向传感器