了解更多,移步Android触摸事件传递机制系列详解
1. MotionEvent 简介
- Android 将所有的输入事件都放在了
MotionEvent
中 - 随着安卓的不断发展壮大,
MotionEvent
也开始变得越来越复杂
MotionEvent 大事记
以上仅仅是简要的说明几次比较大的变动
MotionEvent
负责集中处理所有类型设备的输入事件,但是由于某些设备使用的几率较小本文会忽略讲解,或者简要讲解,例如:
- 轨迹球只出现在最早的设备上,现代的设备上已经见不到了,本文不再叙述。
- 触控笔和手指处理流程基本相同,不再多说。
- 鼠标在手机上使用概率也比较小,会在文末简要介绍。
1.1 事件类型
涉及MotionEvent的事件类型主要有:
public static final int ACTION_DOWN = 0;
public static final int ACTION_UP = 1;
public static final int ACTION_MOVE = 2;
public static final int ACTION_CANCEL = 3;
public static final int ACTION_OUTSIDE = 4;
public static final int ACTION_POINTER_DOWN = 5;
public static final int ACTION_POINTER_UP = 6;
-
ACTION_DOWN
: 第一个手指按下时 -
ACTION_MOVE
:按住一点在屏幕上移动 -
ACTION_UP
:最后一个手指抬起时 -
ACTION_CANCEL
:当前的手势被取消了,并且再也不会接收到后续的触摸事件,这时我们就像ACTION_UP一样对待他以结束该手势操作,但是却不执行我们在ACTION_UP时需要执行的动作。
ViewGroup分发事件的机制。一般来说,如果一个子视图接收了父视图分发给它的ACTION_DOWN事件,那么与ACTION_DOWN事件相关的事件流就都要分发给这个子视图,但是如果父视图希望拦截其中的一些事件,不再继续转发事件给这个子视图的话,那么就需要给子视图一个ACTION_CANCEL事件。这在后续文章中源码分析部分也有体现。 -
ACTION_OUTSIDE
: 表示用户触碰超出了正常的UI边界.
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.
一个触摸事件已经发生了UI元素的正常范围之外。因此不再提供完整的手势,只提供 运动/触摸 的初始位置。
我们知道,正常情况下,如果初始点击位置在该视图区域之外,该视图根本不可能会收到事件,然而,万事万物都不是绝对的,肯定还有一些特殊情况,你可曾还记得点击 Dialog
区域外关闭吗?Dialog
就是一个特殊的视图(没有占满屏幕大小的窗口),能够接收到视图区域外的事件(虽然在通常情况下你根本用不到这个事件),除了 Dialog
之外,你最可能看到这个事件的场景是悬浮窗,当然啦,想要接收到视图之外的事件需要一些特殊的设置。
设置视图的 WindowManager 布局参数的 flags为FLAG_WATCH_OUTSIDE_TOUCH ,这样点击事件发生在这个视图之外时,该视图就可以接收到一个 ACTION_OUTSIDE
事件。
-
ACTION_POINTER_DOWN
:代表用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,又新出现了一个触摸点。 -
ACTION_POINTER_UP
::代表用户的一个手指离开了触摸屏,但是还有其他手指还在触摸屏上。也就是说,在多个触摸点存在的情况下,其中一个触摸点消失了。它与ACTION_UP的区别就是,它是在多个触摸点中的一个触摸点消失时(此时,还有触摸点存在,也就是说用户还有手指触摸屏幕)产生,而8.ACTION_UP
可以说是最后一个触摸点消失时产生。会在多指触摸和Pointers章节详解。
1.2 坐标
event.getX(); //触摸点相对于View左上角为原点坐标系的X坐标
event.getY(); //触摸点相对于View左上角为原点坐标系的Y坐标
event.getRawX(); //触摸点相对于屏幕左上角为原点坐标系的X坐标
event.getRawY(); //触摸点相对于屏幕左上角为原点坐标系的Y坐标
2 单点触控
主要涉及以下几个事件:
- 当我们在操作手机屏幕时,哪怕只是轻轻点击一下,系统也会产生一系列的触摸事件(MotionEvent)对象。具体都会有哪些对象产生和我们的操作密不可分。
- 这个动作所产生的一系列事件,被称为一个事件流,通常包括一个ACTION_DOWN事件,很多个ACTION_MOVE事件,和一个ACTION_UP事件。
轻轻点击一下
触摸手势:0--ACTION_DOWN
触摸手势:1--ACTION_UP
点击按下后滑动一下再抬起
com.zlq.customwidget V/TouchEventTest: 触摸手势:0--ACTION_DOWN
com.zlq.customwidget V/TouchEventTest: 触摸手势:2--ACTION_MOVE
com.zlq.customwidget V/TouchEventTest: 触摸手势:2--ACTION_MOVE
...
com.zlq.customwidget V/TouchEventTest: 触摸手势:1--ACTION_UP
3 多点触控
- Android 在 2.0 版本的时候开始支持多点触控,一旦出现了多点触控,很多东西就突然之间变得麻烦起来了,首先要解决的问题就是 多个手指同时按在屏幕上,会产生很多的事件,这些事件该如何区分呢?
- 为了区分这些事件,工程师们用了一个很简单的办法--编号,当手指第一次按下时产生一个唯一的号码,手指抬起或者事件被拦截就回收编号,就这么简单。
-
第一次按下的手指特殊处理作为主指针,之后按下的手指作为辅助指针,然后随之衍生出来了以下事件(注意增加的事件和事件简介的变化):
和以下方法:
4 getAction()
与 getActionMasked()
- 当多个手指在屏幕上按下的时候,会产生大量的事件,如何在获取事件类型的同时区分这些事件就是一个大问题了。
- 一般来说我们可以通过为事件添加一个int类型的index属性来区分,但是我们知道谷歌工程师是有洁癖的(在 自定义View分类与流程 的onMeasure中已经见识过了),为了添加一个通常数值不会超过10的index属性就浪费一个int大小的空间简直是不能忍受的,于是工程师们将这个index属性和事件类型直接合并了。
- int类型共32位(0x00000000),他们用最低8位(0x000000ff)表示事件类型,再往前的8位(0x0000ff00)表示事件编号,以手指按下为例讲解数值是如何合成的:
ACTION_DOWN 的默认数值为 (0x00000000)
ACTION_POINTER_DOWN 的默认数值为 (0x00000005)
注意:
- 上面表格中用粗体标示出的数值,可以看到随着按下手指数量的增加,这个数值也是一直变化的,进而导致我们使用
getAction()
获取到的数值无法与标准的事件类型进行对比,为了解决这个问题,他们创建了一个getActionMasked()
方法,这个方法可以清除index数值,让其变成一个标准的事件类型。
- 多点触控时必须使用
getActionMasked()
来获取事件类型。 - 单点触控时由于事件数值不变,使用
getAction()
和getActionMasked()
两个方法都可以。 - 使用
getActionIndex(
) 可以获取到这个index数值。不过请注意,getActionIndex()
只在down
和up
时有效,move
时是无效的。
目前来说获取事件类型使用getActionMasked()
就行了,但是如果一定要编译时兼容古董版本的话,可以考虑使用这样的写法:
final int action = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO)
? event.getActionMasked()
: event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
// TODO
break;
}
5 PointId
- 虽然前面刚刚说了一个
actionIndex
,可以使用getActionIndex()
获得,但通过actionIndex
字面意思知道,这个只表示事件的序号,而且根据其说明文档解释,这个ActionIndex
只有在手指按下(down
)和抬起(up
)时是有用的,在移动(move
)时是没有用的,事件追踪非常重要的一环就是移动(move
),然而它却没卵用 -
PointId
在手指按下时产生,手指抬起或者事件被取消后消失,是一个事件流程中唯一不变的标识,可以在手指按下时 通过getPointerId(int pointerIndex)
获得。 (参数中的 pointerIndex 就是 actionIndex)
6 历史数据(批处理)
- 由于我们的设备非常灵敏,手指稍微移动一下就会产生一个移动事件,所以移动事件会产生的特别频繁,为了提高效率,系统会将近期的多个移动事件(
move
)按照事件发生的顺序进行排序打包放在同一个MotionEvent
中,与之对应的产生了以下方法:
注意:
pin
全称是pointerIndex
,表示第几个手指,此处为了节省空间使用了缩写。
- 历史数据只有 ACTION_MOVE 事件。
- 历史数据单点触控和多点触控均可以用。
下面是官方文档给出的一个简单使用示例:
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));
}
}
7 获取事件发生的时间
- pos 表示历史数据中的第几个数据。( pos < getHistorySize() )
- 返回值类型为 long,单位是毫秒。
获取压力(接触面积大小)
MotionEvent支持获取某些输入设备(手指或触控笔)的与屏幕的接触面积和压力大小,主要有以下方法:
pin
全称是pointerIndex
,表示第几个手指。(pin < getPointerCount() )pos
表示历史数据中的第几个数据。( pos < getHistorySize() )
注意:
- 获取接触面积大小和获取压力大小是需要硬件支持的。
- 非常不幸的是大部分设备所使用的电容屏不支持压力检测,但能够大致检测出接触面积。
- 大部分设备的 getPressure() 是使用接触面积来模拟的。
- 由于某些未知的原因(可能系统版本和硬件问题),某些设备不支持该方法。
我用不同的设备对这两个方法进行了测试,然而不同设备测试出来的结果不相同,之后经过我多方查证,发现是系统问题,有的设备上只有 getSize() 能用,有的设备上只有 getPressure() 能用,而有的则两个都不能用。
由于获取接触面积和获取压力大小受系统和硬件影响,使用的时候一定要进行数据检测,以防因为设备问题而导致程序出错。
8 鼠标事件
由于触控笔事件和手指事件处理流程大致相同,所以就不讲解了,这里讲解一下与鼠标相关的几个事件:
1、这些事件类型是 安卓4.0 (API 14) 才添加的。2、使用 getActionMasked()
获得这些事件类型。3、这些事件不会传递到 onTouchEvent(MotionEvent) 而是传递到 onGenericMotionEvent(MotionEvent) 。
9 输入设备类型判断
输入设备类型判断也是安卓4.0 (API 14) 才添加的,主要包括以下几种设备:
使用
getToolType(int pointerIndex)
来获取对应的输入设备类型,pointIndex
可以为0,但必须小于 getPointerCount()
。