简要介绍触摸事件,主要包括 单点触控、多点触控、鼠标事件 以及 getAction() 和 getActionMasked() 的区别
一、单点触控
事件 | 简介 |
ACTION_DOWN | 手指 初次接触到屏幕 时触发。 |
ACTION_MOVE | 手指 在屏幕上滑动 时触发,会多次触发。 |
ACTION_UP | 手指 离开屏幕 时触发。 |
ACTION_CANCEL | 事件 被上层拦截 时触发。 |
ACTION_OUTSIDE | 手指 不在控件区域 时触发。 |
涉及到以下方法:
getAction() //获取事件类型。
getX() //获得触摸点在当前 View 的 X 轴坐标。
getY() //获得触摸点在当前 View 的 Y 轴坐标。
getRawX() //获得触摸点在整个屏幕的 X 轴坐标。
getRawY() //获得触摸点在整个屏幕的 Y 轴坐标。
一次完整的触控事件:手指落下(ACTION_DOWN) -> 多次移动(ACTION_MOVE) -> 离开(ACTION_UP),针对单点触控的事件处理一般是这样写的:
@Override
public boolean onTouchEvent(MotionEvent event) {
// ▼ 注意这里使用的是 getAction(),先埋一个小尾巴。
switch (event.getAction()){
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);
}
我们来看看4.1.40中示例View1在消费事件后的日志:
I/MainActivity: dispatchTouchEvent开始 MotionEvent.ACTION_DOWN
I/RootView: dispatchTouchEvent开始 MotionEvent.ACTION_DOWN
I/RootView: onInterceptTouchEvent开始 MotionEvent.ACTION_DOWN
I/ViewGroupA: dispatchTouchEvent开始 MotionEvent.ACTION_DOWN
I/ViewGroupA: onInterceptTouchEvent开始 MotionEvent.ACTION_DOWN
I/View1: dispatchTouchEvent开始 MotionEvent.ACTION_DOWN
I/View1: onTouchEvent开始 MotionEvent.ACTION_DOWN
I/MainActivity: dispatchTouchEvent开始 MotionEvent.ACTION_MOVE
I/RootView: dispatchTouchEvent开始 MotionEvent.ACTION_MOVE
I/RootView: onInterceptTouchEvent开始 MotionEvent.ACTION_MOVE
I/ViewGroupA: dispatchTouchEvent开始 MotionEvent.ACTION_MOVE
I/ViewGroupA: onInterceptTouchEvent开始 MotionEvent.ACTION_MOVE
I/View1: dispatchTouchEvent开始 MotionEvent.ACTION_MOVE
I/View1: onTouchEvent开始 MotionEvent.ACTION_MOVE
I/MainActivity: dispatchTouchEvent开始 MotionEvent.ACTION_UP
I/RootView: dispatchTouchEvent开始 MotionEvent.ACTION_UP
I/RootView: onInterceptTouchEvent开始 MotionEvent.ACTION_UP
I/ViewGroupA: dispatchTouchEvent开始 MotionEvent.ACTION_UP
I/ViewGroupA: onInterceptTouchEvent开始 MotionEvent.ACTION_UP
I/View1: dispatchTouchEvent开始 MotionEvent.ACTION_UP
I/View1: onTouchEvent开始 MotionEvent.ACTION_UP
1.1 ACTION_CANCEL
ACTION_CANCEL 的触发条件是事件被上层拦截,然而我们在 事件分发机制原理 一文中了解到当事件被上层 View 拦截的时候,ChildView 是收不到任何事件的,ChildView 收不到任何事件,自然也不会收到 ACTION_CANCEL 了,这不是相互违背么?
事实上这里表示的是,当上层 View 回收事件处理权的时候,ChildView 才会收到一个 ACTION_CANCEL 事件。 譬如最开始是ChildView消费了down事件,但是当move和up的过程中,父布局截获了事件并消费,此时chlid会收到 ACTION_CANCEL 事件
例如:
- 上层 View 是一个 RecyclerView,它收到了一个 ACTION_DOWN 事件,由于这个可能是点击事件,所以它先传递给对应 ItemView,询问 ItemView 是否需要这个事件。
- 接下来又传递过来了一个 ACTION_MOVE 事件,且移动的方向和 RecyclerView 的可滑动方向一致,所以 RecyclerView 判断这个事件是滚动事件,于是要收回事件处理权,这时候对应的 ItemView 会收到一个 ACTION_CANCEL ,并且不会再收到后续事件
1.2 ACTION_OUTSIDE
ACTION_OUTSIDE的触发条件更加奇葩,从字面上看,outside 意思不就是超出区域么?然而不论你如何滑动超出控件区域都不会触发 ACTION_OUTSIDE 这个事件
正常情况下,如果初始点击位置在该视图区域之外,该视图根本不可能会收到事件,然而,万事万物都不是绝对的,肯定还有一些特殊情况,你可曾还记得点击 Dialog 区域外关闭吗?Dialog 就是一个特殊的视图(没有占满屏幕大小的窗口),能够接收到视图区域外的事件(虽然在通常情况下你根本用不到这个事件),除了 Dialog 之外,你最可能看到这个事件的场景是悬浮窗,当然啦,想要接收到视图之外的事件需要一些特殊的设置
设置视图的 WindowManager 布局参数的 flags为FLAG_WATCH_OUTSIDE_TOUCH,这样点击事件发生在这个视图之外时,该视图就可以接收到一个 ACTION_OUTSIDE 事件。
参见StackOverflow:How to dismiss the dialog with click on outside of the dialog?
1.3 move历史数据(批处理)
由于我们的设备非常灵敏,手指稍微移动一下就会产生一个移动事件,所以移动事件会产生的特别频繁,为了提高效率,系统会将近期的多个移动事件(move)按照事件发生的顺序进行排序打包放在同一个 MotionEvent 中,与之对应的产生了以下方法:
事件 | 简介 |
getHistorySize() | 获取历史事件集合大小 |
getHistoricalX(int pos) | 获取第pos个历史事件x坐标(pos < getHistorySize()) |
getHistoricalY(int pos) | 获取第pos个历史事件y坐标(pos < getHistorySize()) |
getHistoricalX (int pin, int pos) | 获取第pin个手指的第pos个历史事件x坐标(pin < getPointerCount(), pos < getHistorySize() ) |
getHistoricalY (int pin, int pos) | 获取第pin个手指的第pos个历史事件y坐标(pin < getPointerCount(), pos < getHistorySize() ) |
- pin 全称是 pointerIndex,表示第几个手指,此处为了节省空间使用了缩写。
- 历史数据只有 ACTION_MOVE 事件。
- 历史数据单点触控和多点触控均可以用
二、多点触控
首先要解决的问题就是 多个手指同时按在屏幕上,会产生很多的事件,这些事件该如何区分呢?为了区分这些事件,工程师们用了一个很简单的办法—编号: 当手指第一次按下时产生一个唯一的号码,手指抬起或者事件被拦截就回收编号,就这么简单。
第一次按下的手指特殊处理作为主指针,之后按下的手指作为辅助指针,然后随之衍生出来了以下事件(注意增加的事件和事件简介的变化):
事件 | 简介 |
ACTION_DOWN | 第一个 手指 初次接触到屏幕 时触发。 |
ACTION_MOVE | 手指 在屏幕上滑动 时触发,会多次触发。 |
ACTION_UP | 最后一个 手指 离开屏幕 时触发。 |
ACTION_POINTER_DOWN | 有非主要的手指按下(即按下之前已经有手指在屏幕上)。 |
ACTION_POINTER_UP | 有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。 |
以下事件类型不推荐使用 | ------------------ |
ACTION_POINTER_1_DOWN | 第 2 个手指按下,已废弃,不推荐使用。 |
ACTION_POINTER_2_DOWN | 第 3 个手指按下,已废弃,不推荐使用。 |
ACTION_POINTER_3_DOWN | 第 4 个手指按下,已废弃,不推荐使用。 |
ACTION_POINTER_1_UP | 第 2 个手指抬起,已废弃,不推荐使用。 |
ACTION_POINTER_2_UP | 第 3 个手指抬起,已废弃,不推荐使用。 |
ACTION_POINTER_3_UP | 第 4 个手指抬起,已废弃,不推荐使用。 |
getActionMasked()//与 getAction() 类似,多点触控必须使用这个方法获取事件类型。
getActionIndex()//获取该事件是哪个指针(手指)产生的。
getPointerCount()//获取在屏幕上手指的个数。
getPointerId(int pointerIndex)//获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
findPointerIndex(int pointerId)//通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。
getX(int pointerIndex)//获取某一个指针(手指)的X坐标
getY(int pointerIndex)//获取某一个指针(手指)的Y坐标
//int action = event.getAction() & MotionEvent.ACTION_MASK;
//int index = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
int index = event.getActionIndex();//2.2以上版本支持
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"第1个手指按下");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"最后1个手指抬起");
break;
case MotionEvent.ACTION_POINTER_DOWN:
Log.e(TAG,"第"+(index+1)+"个手指按下");
break;
case MotionEvent.ACTION_POINTER_UP:
Log.e(TAG,"第"+(index+1)+"个手指抬起");
break;
}
2.1 getAction() 与 getActionMasked()
- getActionMasked() 方法,这个方法可以清除index数值,让其变成一个标准的事件类型
- 多点触控时必须使用 getActionMasked() 来获取事件类型
- 单点触控时由于事件数值不变,使用 getAction() 和 getActionMasked() 两个方法都可以
- getAction() 获取事件类型:类型 + 编号
- getActionIndex() 获取事件编号
- getActionIndex() 只在 down 和 up 时有效,move 时是无效的
当多个手指在屏幕上按下的时候,会产生大量的事件,如何在获取事件类型的同时区分这些事件就是一个大问题了
int类型共32位(0x00000000),他们用最低8位(0x000000ff)表示事件类型,再往前的8位(0x0000ff00)表示事件编号,以手指按下为例讲解数值是如何合成的:
手指按下 | 触发事件(数值) |
第1个手指按下 | ACTION_DOWN (0x00000000) |
第2个手指按下 | ACTION_POINTER_DOWN (0x00000105) |
第3个手指按下 | ACTION_POINTER_DOWN (0x00000205) |
第4个手指按下 | ACTION_POINTER_DOWN (0x00000305) |
2.2 index 和 pointId 的变化规则
在 2.2 版本以上,我们可以通过 getActionIndex() 轻松获取到事件的索引(Index),但是这个事件索引的变化还是有点意思的,Index 变化有以下几个特点:
- 从 0 开始,自动增长。
- 如果之前落下的手指抬起,后面手指的 Index 会随之减小。
- Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。
- 对 move 事件无效。
参考文献