|
1 | | -# MotionEvent |
| 1 | +# Android MotionEvent 详解 |
| 2 | + |
| 3 | +Android MotionEvent 详解,之前用了两篇文章 [事件分发机制原理][customview/dispatch-touchevent-theory] 和 [事件分发机制详解][customview/dispatch-touchevent-source] 来讲解事件分发,而作为事件分发主角之一的 MotionEvent 并没有过多的说明,本文就带大家了解 MotionEvent 的相关内容,简要介绍触摸事件,主要包括 单点触控、多点触控、鼠标事件 以及 getAction() 和 getActionMasked() 的区别。 |
| 4 | + |
| 5 | +Android 将所有的输入事件都放在了 MotionEvent 中,随着安卓的不断发展壮大,MotionEvent 也开始变得越来越复杂,下面是我自己整理的 MotionEvent 大事记: |
| 6 | + |
| 7 | +| 版本号 | 更新内容 | |
| 8 | +| :-------------------: | --------------------------- | |
| 9 | +| Android 1.0 (API 1 ) | 支持单点触控和轨迹球的事件。 | |
| 10 | +| Android 1.6 (API 4 ) | 支持手势。 | |
| 11 | +| Android 2.2 (API 8 ) | 支持多点触控。 | |
| 12 | +| Android 3.1 (API 12) | 支持触控笔,鼠标,键盘,操纵杆,游戏控制器等输入工具。 | |
| 13 | + |
| 14 | +以上仅仅是简要的说明几次比较大的变动,细小的修复和更新不计其数,此处就不一一列出了,反正也没人关心这些东西。 |
| 15 | +MotionEvent 负责集中处理所有类型设备的输入事件,但是由于某些设备使用的几率较小本文会忽略讲解,或者简要讲解,例如: |
| 16 | +1、轨迹球只出现在最早的设备上,现代的设备上已经见不到了,本文不再叙述。 |
| 17 | +2、触控笔和手指处理流程基本相同,不再多说。 |
| 18 | +3、鼠标在手机上使用概率也比较小,会在文末简要介绍。 |
| 19 | + |
| 20 | + |
| 21 | + |
| 22 | +## 单点触控 |
| 23 | + |
| 24 | +单点触控就非常简单啦,入门的工程师都会用,上一篇文章也简要介绍过,主要涉及以下几个事件: |
| 25 | + |
| 26 | +| 事件 | 简介 | |
| 27 | +| -------------- | ------------------------ | |
| 28 | +| ACTION_DOWN | 手指 **初次接触到屏幕** 时触发。 | |
| 29 | +| ACTION_MOVE | 手指 **在屏幕上滑动** 时触发,会多次触发。 | |
| 30 | +| ACTION_UP | 手指 **离开屏幕** 时触发。 | |
| 31 | +| ACTION_CANCEL | 事件 **被上层拦截** 时触发。 | |
| 32 | +| ACTION_OUTSIDE | 手指 **不在控件区域** 时触发。 | |
| 33 | + |
| 34 | +和以下的几个方法: |
| 35 | + |
| 36 | +| 方法 | 简介 | |
| 37 | +| :---------- | ---------------------- | |
| 38 | +| getAction() | 获取事件类型。 | |
| 39 | +| getX() | 获得触摸点在当前 View 的 X 轴坐标。 | |
| 40 | +| getY() | 获得触摸点在当前 View 的 Y 轴坐标。 | |
| 41 | +| getRawX() | 获得触摸点在整个屏幕的 X 轴坐标。 | |
| 42 | +| getRawY() | 获得触摸点在整个屏幕的 Y 轴坐标。 | |
| 43 | + |
| 44 | +> 关于 `get` 和 `getRaw` 的区别可以参考这一篇文章 [安卓自定义View基础-坐标系][customview/CoordinateSystem] |
| 45 | +
|
| 46 | +单点触控一次简单的交互流程是这样的: |
| 47 | + |
| 48 | +**手指落下(ACTION_DOWN) -> 多次移动(ACTION_MOVE) -> 离开(ACTION_UP)** |
| 49 | + |
| 50 | +> - 本次事例中 ACTION_MOVE 有多次触发。 |
| 51 | +> - 如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。 |
| 52 | +
|
| 53 | + |
| 54 | + |
| 55 | +针对单点触控的事件处理一般是这样写的: |
| 56 | + |
| 57 | +```java |
| 58 | +@Override |
| 59 | +public boolean onTouchEvent(MotionEvent event) { |
| 60 | + // ▼ 注意这里使用的是 getAction(),先埋一个小尾巴。 |
| 61 | + switch (event.getAction()){ |
| 62 | + case MotionEvent.ACTION_DOWN: |
| 63 | + // 手指按下 |
| 64 | + break; |
| 65 | + case MotionEvent.ACTION_MOVE: |
| 66 | + // 手指移动 |
| 67 | + break; |
| 68 | + case MotionEvent.ACTION_UP: |
| 69 | + // 手指抬起 |
| 70 | + break; |
| 71 | + case MotionEvent.ACTION_CANCEL: |
| 72 | + // 事件被拦截 |
| 73 | + break; |
| 74 | + case MotionEvent.ACTION_OUTSIDE: |
| 75 | + // 超出区域 |
| 76 | + break; |
| 77 | + } |
| 78 | + return super.onTouchEvent(event); |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +相信小伙伴对此已经非常熟悉了,经常使用的东西,我也不啰嗦了。 |
| 83 | + |
| 84 | +但其中有两个比较特殊的事件: `ACTION_CANCEL` 和 `ACTION_OUTSIDE` 。 |
| 85 | +为什么说特殊呢,因为它们是由程序触发而产生的,而且触发条件也非常特殊,通常情况下即便不处理这两个事件也没有什么问题。接下来我们就扒一扒它们的真面目: |
| 86 | + |
| 87 | +### ACTION_CANCEL |
| 88 | + |
| 89 | +**`ACTION_CANCEL` 的触发条件是事件被上层拦截**,然而我们在 [事件分发机制原理][customview/dispatch-touchevent-theory] 一文中了解到当事件被上层 View 拦截的时候,ChildView 是收不到任何事件的,ChildView 收不到任何事件,自然也不会收到 `ACTION_CANCEL` 了,所以说这个 `ACTION_CANCEL` 的正确触发条件并不是这样,那么是什么呢? |
| 90 | + |
| 91 | +**事实上,只有上层 View 回收事件处理权的时候,ChildView 才会收到一个 `ACTION_CANCEL` 事件。** |
| 92 | + |
| 93 | +这样说可能不太容易不理解,咱举个例子? |
| 94 | + |
| 95 | + |
| 96 | + |
| 97 | +> 例如:上层 View 是一个 RecyclerView,它收到了一个 `ACTION_DOWN` 事件,由于这个可能是点击事件,所以它先传递给对应 ItemView,询问 ItemView 是否需要这个事件,然而接下来又传递过来了一个 `ACTION_MOVE` 事件,且移动的方向和 RecyclerView 的可滑动方向一致,所以 RecyclerView 判断这个事件是滚动事件,于是要收回事件处理权,这时候对应的 ItemView 会收到一个 `ACTION_CANCEL` ,并且不会再收到后续事件。 |
| 98 | +> |
| 99 | +> **通俗一点?** |
| 100 | +> |
| 101 | +> RecyclerView:儿砸,这里有一个 `ACTION_DOWN` 你看你要不要。 |
| 102 | +> ItemView :好嘞,我看看。 |
| 103 | +> RecyclerView:噫?居然是移动事件`ACTION_MOVE`,我要滚起来了,儿砸,我可能要把你送去你姑父家(缓存区)了,在这之前给你一个 `ACTION_CANCEL`,你要收好啊。 |
| 104 | +> ItemView :…... |
| 105 | +> |
| 106 | +> 这是实际开发中最有可能见到 `ACTION_CANCEL` 的场景了。 |
| 107 | +
|
| 108 | + |
| 109 | + |
| 110 | +### ACTION_OUTSIDE |
| 111 | + |
| 112 | +`ACTION_OUTSIDE`的触发条件更加奇葩,从字面上看,outside 意思不就是超出区域么?然而不论你如何滑动超出控件区域都不会触发 `ACTION_OUTSIDE` 这个事件。相信很多魔法师都对此很是疑惑,说好的超出区域呢? |
| 113 | + |
| 114 | +实际上这个事件根本就不是在这里用的,看官方解释(装一下逼): |
| 115 | + |
| 116 | +> 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. |
| 117 | +> |
| 118 | +> 一个触摸事件已经发生了UI元素的正常范围之外。因此不再提供完整的手势,只提供 运动/触摸 的初始位置。 |
| 119 | +
|
| 120 | +我们知道,正常情况下,如果初始点击位置在该视图区域之外,该视图根本不可能会收到事件,然而,万事万物都不是绝对的,肯定还有一些特殊情况,你可曾还记得点击 Dialog 区域外关闭吗?Dialog 就是一个特殊的视图(没有占满屏幕大小的窗口),能够接收到视图区域外的事件(虽然在通常情况下你根本用不到这个事件),除了 Dialog 之外,你最可能看到这个事件的场景是悬浮窗,当然啦,想要接收到视图之外的事件需要一些特殊的设置。 |
| 121 | + |
| 122 | +> 设置视图的 WindowManager 布局参数的 flags为[`FLAG_WATCH_OUTSIDE_TOUCH`](http://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_WATCH_OUTSIDE_TOUCH),这样点击事件发生在这个视图之外时,该视图就可以接收到一个 `ACTION_OUTSIDE` 事件。 |
| 123 | +> |
| 124 | +> 参见StackOverflow:[How to dismiss the dialog with click on outside of the dialog?](http://stackoverflow.com/questions/8384067/how-to-dismiss-the-dialog-with-click-on-outside-of-the-dialog) |
| 125 | +
|
| 126 | +由于这个事件用到的几率比较小,此处就不展开叙述了,以后用到的时候再详细讲解。 |
| 127 | + |
| 128 | + |
| 129 | + |
| 130 | +## 多点触控 |
| 131 | + |
| 132 | +Android 在 2.2 版本的时候开始支持多点触控,一旦出现了多点触控,很多东西就突然之间变得麻烦起来了,首先要解决的问题就是 **多个手指同时按在屏幕上,会产生很多的事件,这些事件该如何区分呢?** |
| 133 | + |
| 134 | +为了区分这些事件,工程师们用了一个很简单的办法--**编号,当手指第一次按下时产生一个唯一的号码,手指抬起或者事件被拦截就回收编号,就这么简单。** |
| 135 | + |
| 136 | +**第一次按下的手指特殊处理作为主指针,之后按下的手指作为辅助指针**,然后随之衍生出来了以下事件(注意增加的事件和事件简介的变化): |
| 137 | + |
| 138 | +| 事件 | 简介 | |
| 139 | +| --------------------------- | ------------------------------ | |
| 140 | +| ACTION_DOWN | **第一个** 手指 **初次接触到屏幕** 时触发。 | |
| 141 | +| ACTION_MOVE | 手指 **在屏幕上滑动** 时触发,会多次触发。 | |
| 142 | +| ACTION_UP | **最后一个** 手指 **离开屏幕** 时触发。 | |
| 143 | +| **ACTION_POINTER_DOWN** | 有非主要的手指按下(**即按下之前已经有手指在屏幕上**)。 | |
| 144 | +| **ACTION_POINTER_UP** | 有非主要的手指抬起(**即抬起之后仍然有手指在屏幕上**)。 | |
| 145 | +| 以下事件类型不推荐使用 | ------------------ | |
| 146 | +| ~~ACTION_POINTER\_1\_DOWN~~ | 第 2 个手指按下,已废弃,不推荐使用。 | |
| 147 | +| ~~ACTION_POINTER\_2\_DOWN~~ | 第 3 个手指按下,已废弃,不推荐使用。 | |
| 148 | +| ~~ACTION_POINTER\_3\_DOWN~~ | 第 4 个手指按下,已废弃,不推荐使用。 | |
| 149 | +| ~~ACTION_POINTER\_1\_UP~~ | 第 2 个手指抬起,已废弃,不推荐使用。 | |
| 150 | +| ~~ACTION_POINTER\_2\_UP~~ | 第 3 个手指抬起,已废弃,不推荐使用。 | |
| 151 | +| ~~ACTION_POINTER\_3\_UP~~ | 第 4 个手指抬起,已废弃,不推荐使用。 | |
| 152 | + |
| 153 | +和以下方法: |
| 154 | + |
| 155 | +| 方法 | 简介 | |
| 156 | +| ------------------------------- | ---------------------------------------- | |
| 157 | +| getActionMasked() | 与 `getAction()` 类似,**多点触控必须使用这个方法获取事件类型**。 | |
| 158 | +| getActionIndex() | 获取该事件是哪个指针(手指)产生的。 | |
| 159 | +| getPointerCount() | 获取在屏幕上手指的个数。 | |
| 160 | +| getPointerId(int pointerIndex) | 获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。 | |
| 161 | +| findPointerIndex(int pointerId) | 通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。 | |
| 162 | +| getX(int pointerIndex) | 获取某一个指针(手指)的X坐标 | |
| 163 | +| getY(int pointerIndex) | 获取某一个指针(手指)的Y坐标 | |
| 164 | + |
| 165 | +由于多点触控部分涉及内容比较多,也很复杂,我准备单独用一篇文章进行详细叙述,所以这里只叙述一些基础的内容作为铺垫: |
| 166 | + |
| 167 | +### getAction() 与 getActionMasked() |
| 168 | + |
| 169 | +当多个手指在屏幕上按下的时候,会产生大量的事件,如何在获取事件类型的同时区分这些事件就是一个大问题了。 |
| 170 | + |
| 171 | +一般来说我们可以通过为事件添加一个int类型的index属性来区分,但是我们知道谷歌工程师是有洁癖的(在 [自定义View分类与流程][customview/CustomViewProcess] 的onMeasure中已经见识过了),为了添加一个通常数值不会超过10的index属性就浪费一个int大小的空间简直是不能忍受的,于是工程师们将这个index属性和事件类型直接合并了。 |
| 172 | + |
| 173 | +int类型共32位(0x00000000),他们用最低8位(0x000000**ff**)表示事件类型,再往前的8位(0x0000**ff**00)表示事件编号,以手指按下为例讲解数值是如何合成的: |
| 174 | + |
| 175 | +> ACTION_DOWN 的默认数值为 (0x00000000) |
| 176 | +> ACTION_POINTER_DOWN 的默认数值为 (0x00000005) |
| 177 | +
|
| 178 | +| 手指按下 | 触发事件(数值) | |
| 179 | +| :-----: | :--------------------------------------- | |
| 180 | +| 第1个手指按下 | ACTION_DOWN (0x0000**00**00) | |
| 181 | +| 第2个手指按下 | ACTION_POINTER_DOWN (0x0000**01**05) | |
| 182 | +| 第3个手指按下 | ACTION_POINTER_DOWN (0x0000**02**05) | |
| 183 | +| 第4个手指按下 | ACTION_POINTER_DOWN (0x0000**03**05) | |
| 184 | + |
| 185 | +**注意:** |
| 186 | +上面表格中用粗体标示出的数值,可以看到随着按下手指数量的增加,这个数值也是一直变化的,进而导致我们使用 `getAction()` 获取到的数值无法与标准的事件类型进行对比,为了解决这个问题,他们创建了一个 `getActionMasked()` 方法,这个方法可以清除index数值,让其变成一个标准的事件类型。 |
| 187 | +**1、多点触控时必须使用 `getActionMasked()` 来获取事件类型。** |
| 188 | +**2、单点触控时由于事件数值不变,使用 `getAction()` 和 `getActionMasked()` 两个方法都可以。** |
| 189 | +**3、使用 getActionIndex() 可以获取到这个index数值。不过请注意,getActionIndex() 只在 down 和 up 时有效,move 时是无效的。** |
| 190 | + |
| 191 | +### PointId |
| 192 | + |
| 193 | +虽然前面刚刚说了一个 actionIndex,可以使用 getActionIndex() 获得,但通过 actionIndex 字面意思知道,这个只表示事件的序号,而且根据其说明文档解释,这个 ActionIndex 只有在手指按下(down)和抬起(up)时是有用的,在移动(move)时是没有用的,事件追踪非常重要的一环就是移动(move),然而它却没卵用,这也太不实在了 ( ̄Д ̄)ノ |
| 194 | + |
| 195 | +**郑重声明:追踪事件流,请认准 PointId,这是唯一官方指定标准,不要相信 ActionIndex 那个小婊砸。** |
| 196 | + |
| 197 | +PointId 在手指按下时产生,手指抬起或者事件被取消后消失,是一个事件流程中唯一不变的标识,可以在手指按下时 通过 `getPointerId(int pointerIndex)` 获得。 (参数中的 pointerIndex 就是 actionIndex) |
| 198 | + |
| 199 | +关于事件流的追踪等问题在讲解多点触控时再详细讲解。 |
| 200 | + |
| 201 | + |
| 202 | + |
| 203 | +## 历史数据(批处理) |
| 204 | + |
| 205 | +由于我们的设备非常灵敏,手指稍微移动一下就会产生一个移动事件,所以移动事件会产生的特别频繁,为了提高效率,设计师会将近期的多个移动事件(move)按照事件发生的顺序进行排序打包放在同一个 MotionEvent 中,与之对应的产生了以下方法: |
| 206 | + |
| 207 | +| 事件 | 简介 | |
| 208 | +| --------------------------------- | ---------------------------------------- | |
| 209 | +| getHistorySize() | 获取历史事件集合大小 | |
| 210 | +| getHistoricalX(int pos) | 获取第pos个历史事件x坐标<br/> (pos < getHistorySize()) | |
| 211 | +| getHistoricalY(int pos) | 获取第pos个历史事件y坐标<br/> (pos < getHistorySize()) | |
| 212 | +| getHistoricalX (int pin, int pos) | 获取第pin个手指的第pos个历史事件x坐标<br/> (pin < getPointerCount(), pos < getHistorySize() ) | |
| 213 | +| getHistoricalY (int pin, int pos) | 获取第pin个手指的第pos个历史事件y坐标<br/> (pin < getPointerCount(), pos < getHistorySize() ) | |
| 214 | + |
| 215 | +**注意:** |
| 216 | + |
| 217 | +1. pin 全称是 pointerIndex,表示第几个手指,此处为了节省空间使用了缩写。 |
| 218 | +2. 历史数据只有 ACTION_MOVE 事件。 |
| 219 | +3. 历史数据单点触控和多点触控均可以用。 |
| 220 | + |
| 221 | +下面是官方文档给出的一个简单使用示例: |
| 222 | + |
| 223 | +```java |
| 224 | +void printSamples(MotionEvent ev) { |
| 225 | + final int historySize = ev.getHistorySize(); |
| 226 | + final int pointerCount = ev.getPointerCount(); |
| 227 | + for (int h = 0; h < historySize; h++) { |
| 228 | + System.out.printf("At time %d:", ev.getHistoricalEventTime(h)); |
| 229 | + for (int p = 0; p < pointerCount; p++) { |
| 230 | + System.out.printf(" pointer %d: (%f,%f)", |
| 231 | + ev.getPointerId(p), ev.getHistoricalX(p, h), ev.getHistoricalY(p, h)); |
| 232 | + } |
| 233 | + } |
| 234 | + System.out.printf("At time %d:", ev.getEventTime()); |
| 235 | + for (int p = 0; p < pointerCount; p++) { |
| 236 | + System.out.printf(" pointer %d: (%f,%f)", |
| 237 | + ev.getPointerId(p), ev.getX(p), ev.getY(p)); |
| 238 | + } |
| 239 | +} |
| 240 | +``` |
| 241 | + |
| 242 | + |
| 243 | + |
| 244 | + |
| 245 | + |
| 246 | +## 鼠标事件 |
| 247 | + |
| 248 | +由于触控笔事件和手指事件处理流程大致相同,所以就不讲解了,这里讲解一下与鼠标相关的几个事件: |
| 249 | + |
| 250 | +| 事件 | 简介 | |
| 251 | +| ------------------ | ---------------------------------------- | |
| 252 | +| ACTION_HOVER_ENTER | 指针移入到窗口或者View区域,但没有按下。 | |
| 253 | +| ACTION_HOVER_MOVE | 指针在窗口或者View区域移动,但没有按下。 | |
| 254 | +| ACTION_HOVER_EXIT | 指针移出到窗口或者View区域,但没有按下。 | |
| 255 | +| ACTION_SCROLL | 滚轮滚动,可以触发水平滚动(AXIS_HSCROLL)或者垂直滚动(AXIS_VSCROLL) | |
| 256 | + |
| 257 | +注意: |
| 258 | + |
| 259 | +1、这些事件类型是 安卓4.0 (API 14) 才添加的。 |
| 260 | +2、使用 ` getActionMasked()` 获得这些事件类型。 |
| 261 | +3、这些事件不会传递到 [onTouchEvent(MotionEvent)](https://developer.android.com/reference/android/view/View.html#onTouchEvent(android.view.MotionEvent)) 而是传递到 [onGenericMotionEvent(MotionEvent)](https://developer.android.com/reference/android/view/View.html#onGenericMotionEvent(android.view.MotionEvent)) 。 |
| 262 | + |
| 263 | + |
| 264 | + |
| 265 | +## 输入设备类型判断 |
| 266 | + |
| 267 | +输入设备类型判断也是安卓4.0 (API 14) 才添加的,主要包括以下几种设备: |
| 268 | + |
| 269 | +| 设备类型 | 简介 | |
| 270 | +| ----------------- | ---- | |
| 271 | +| TOOL_TYPE_ERASER | 橡皮擦 | |
| 272 | +| TOOL_TYPE_FINGER | 手指 | |
| 273 | +| TOOL_TYPE_MOUSE | 鼠标 | |
| 274 | +| TOOL_TYPE_STYLUS | 手写笔 | |
| 275 | +| TOOL_TYPE_UNKNOWN | 未知类型 | |
| 276 | + |
| 277 | +**使用 `getToolType(int pointerIndex)` 来获取对应的输入设备类型,pointIndex可以为0,但必须小于 `getPointerCount()`。** |
| 278 | + |
| 279 | + |
| 280 | + |
| 281 | +## 总结 |
| 282 | + |
| 283 | +虽然本文标题是 MotionEvent 详解,但由于 MotionEvent 实在太庞大了,本文只能涉及一些比较常用的内容,某些不太常用的内容就在以后用到的时候再详细介绍吧,像游戏手柄等输入设备由于我暂时不做游戏开发,也没有过多了解,所以就不介绍给大家啦。 |
| 284 | + |
| 285 | +由于个人水平有限,文章中可能会出现错误,如果你觉得哪一部分有错误,或者发现了错别字等内容,欢迎在评论区告诉我,另外,据说关注 [作者微博](http://weibo.com/GcsSloop) 不仅能第一时间收到新文章消息,还能变帅哦。 |
| 286 | + |
| 287 | + |
| 288 | + |
| 289 | +## 参考资料 |
| 290 | + |
| 291 | +[MotionEvent ](https://developer.android.com/reference/android/view/MotionEvent.html) |
| 292 | +[Android MotionEvent详解](http://www.jianshu.com/p/0c863bbde8eb) |
| 293 | + |
| 294 | + |
| 295 | + |
| 296 | +## About Me |
| 297 | + |
| 298 | +### 作者微博: <a href="http://weibo.com/GcsSloop" target="_blank">@GcsSloop</a> |
| 299 | + |
| 300 | +<a href="http://www.gcssloop.com/info/about" target="_blank"><img src="http://ww4.sinaimg.cn/large/005Xtdi2gw1f1qn89ihu3j315o0dwwjc.jpg" width="300" style="display:inline;" /></a> |
| 301 | + |
| 302 | + |
| 303 | + |
| 304 | +[customview/CoordinateSystem]: http://www.gcssloop.com/customview/CoordinateSystem "安卓自定义View基础-坐标系" |
| 305 | +[customview/CustomViewProcess]: http://www.gcssloop.com/customview/CustomViewProcess "安卓自定义View进阶-分类与流程" |
| 306 | +[customview/dispatch-touchevent-theory]: http://www.gcssloop.com/customview/dispatch-touchevent-theory "安卓自定义View进阶-事件分发机制原理" |
| 307 | +[customview/dispatch-touchevent-source]: http://www.gcssloop.com/customview/dispatch-touchevent-source "安卓自定义View进阶-事件分发机制详解" |
0 commit comments