MotionEvent详解

3,342 阅读9分钟

简介

大家都知道事件分发拦截与消费在安卓应用中十分普遍.作为事件分发主要传递事件的的MotionEvent类,在很长一段时间我们是知道,但是概念却很模糊.今天我就分享一下关于MotionEvent的使用及其内部的一些基本原理

历史

安卓将所有事件都放在了MotionEvent中,随着安卓的不断发展壮大,MotionEvent也变得越来越复杂.我们先来回顾一下MotionEvent的版本历史.

版本号
更新内容
Android 1.0 (API 1)单点触控和轨迹球事件
Android 1.6 (API 4)增加手势支持
Android 2.0 (API 5)增加多点触控支持
Android 2.2 (API 8)重新设计多点触控,增加getActionMasked()
Android 3.1 (API 12)增加支持触控笔,鼠标,键盘,操纵杆,游戏控制器等输入工具。
  • 轨迹球事件

    只存在安卓早期版本

  • 触控笔

    与单点触控类似

  • 鼠标,键盘操纵杆游戏控制器等

    应用场景很少,基本app开发用不到

下面我们针对MotionEvent的单点触控多点触控及手势方面的api进行一些简单的介绍

单点触控相关

单点触控涉及到的事件

事件
介绍
ACTION_DOWN手指初次碰到屏幕时触发.
ACTION_MOVE手指在屏幕上滑动时触发,会多次触发.
ACTION_UP手指离开屏幕时触发.
ACTION_CANCEL事件被上层拦截时触发。
ACTION_OUTSIDE手指不在触控区域时触发

我们可以看到,单点触控涉及到的事件其实很简单.最主要的是前面三个,手指初次按下时,手指在屏幕上移动时,手指离开屏幕时.

一个简单的单点触控流程是

  1. 手指放到屏幕上 (对应ACTION_DOWN)
  2. 手指在屏幕上移动 (对应ACTION_MOVE)
  3. 手指离开屏幕 (对应ACTION_UP)

单点触控涉及到的方法

方法名
介绍
getAction()获取事件的类型
getX()获取触摸点相对于自己View的X坐标
getY()获取触摸点相对于自己View的Y坐标
getRawX()获取触摸点相对于整个屏幕的X坐标
getRawY()获取触摸点相对于整个屏幕的Y坐标

单点触控的注意事项

上面讲了单点触控所涉及到的事件与方法.但是当我们使用时需要知道一些特点:

  1. 并不是每次都执行action_move事件,当手指快速按下离开时action_move事件是不会执行的
  2. 并不是每次都执行action_down事件后,抬起手指都会走ACTION_UP事件(onTouchEvent 返回false时,后续事件包括action_move和action_up都不会得到执行了)
  3. 假如子view action_down执行后父view 拦截了事件(父 View 回收事件处理权),此时子view会执行action_cancel

单点触控操作的一般流程

@Override
public boolean onTouchEvent(MotionEvent event) {
    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);
}

多点触控相关

2.0版本增加的多点触控相关介绍

我们知道,单点触控的话我们仅仅关注一个手指的按下移动和点击,那么多个手指点击的情况下,事情就变得复杂起来了.我们需要区分不同手指的不同动作.这些事情,安卓是怎么实现的呢?我们来看一下MotionEvent关于多点触控支持的事件和方法.

  • 2.0相关事件

此处排除ACTION_CANCEL,和ACTION_OUTSIDE等无用事件

事件
介绍
ACTION_DOWN第一个 手指 初次接触到屏幕 时触发
ACTION_MOVE手指 在屏幕上滑动 时触发
ACTION_UP最后一个 手指 离开屏幕 时触发
ACTION_POINTER_DOWN非主要的手指 按下(即按下之前已经有手指在屏幕上)。 (安卓2.0新增)
ACTION_POINTER_UP非主要的手指 抬起(即抬起之后仍然有手指在屏幕上)。 (安卓2.0新增)
ACTION_POINTER_1_DOWN2 个手指按下时触发 (安卓2.0新增)
ACTION_POINTER_2_DOWN3 个手指按下时触发 (安卓2.0新增)
ACTION_POINTER_3_DOWN4 个手指按下时触发 (安卓2.0新增)
ACTION_POINTER_1_UP2 个手指抬起时触发 (安卓2.0新增)
ACTION_POINTER_2_UP3 个手指抬起时触发 (安卓2.0新增)
ACTION_POINTER_3_UP4 个手指抬起时触发 (安卓2.0新增)
  • 2.0相关方法 |
    方法名
    |
    介绍
    | |-|-| |getAction()|获取事件的类型| |getX()|获取触摸点相对于自己View的X坐标| |getY()|获取触摸点相对于自己View的Y坐标| |getX(int pointerIndex)| 新增 根据传入的 pointerIndex 获取触摸点相对于自己View的X坐标| |getY(int pointerIndex)| 新增 根据传入的 pointerIndex 获取触摸点相对于自己View的Y坐标| |getRawX()|获取触摸点相对于整个屏幕的X坐标| |getRawY()|获取触摸点相对于整个屏幕的Y坐标| |getPointerCount()| 新增 获取在屏幕上触摸的总手指数量| |getPointerId(int pointerIndex)| 新增 根据pointerIndex获取pointerId| |findPointerIndex(int pointerId)| 新增 根据pointerId获取pointerIndex|

我们看到在2.0版本增加多点触控后,增加了诸多对应的事件和相关方法,同时我们也抛出一个问题,那就是2.0版本的多点触控解决方案只支持用户的4个手指同时操作,超过4个手指的情况下,app开发者无法感知,也无法处理.同时我们在开发过程中会发现使用过于繁琐,swich case里面需要处理大量分支,及相同逻辑代码.

2.2升级多点触控后相关介绍

  • 2.2之后相关事件的改变
事件
介绍
ACTION_POINTER_1_DOWN2.2之后废弃2 个手指按下时触发 (安卓2.0新增)
ACTION_POINTER_2_DOWN2.2之后废弃3 个手指按下时触发 (安卓2.0新增)
ACTION_POINTER_3_DOWN2.2之后废弃4 个手指按下时触发 (安卓2.0新增)
ACTION_POINTER_1_UP2.2之后废弃2 个手指抬起时触发 (安卓2.0新增)
ACTION_POINTER_2_UP2.2之后废弃3 个手指抬起时触发 (安卓2.0新增)
ACTION_POINTER_3_UP2.2之后废弃4 个手指抬起时触发 (安卓2.0新增)
  • 2.2之后相关方法的改变 |
    方法名
    |
    介绍
    | |-|-| |getActionMasked()| 新增 与 getAction() 类似,多点触控必须使用这个方法获取事件类型。| |getActionIndex()| 新增 获取该事件是哪个指针(手指)产生的。|

我们可以看到,2.2版本后面主要废弃了以前的非主要手指的按下和抬起事件,增加了getActionMasked()和getActionIndex(),来替换前面的废弃的几个事件常量.从而简化非主要手指的控制流程,减少多点触控事件操作难度和上面提到的问题.2.2版本也就是之后最终版本google给我们提供的多点触控方案.他推荐让我们用getActionMasked()获取事件类型,getActionIndex()获取第几个手指,从ACTION_POINTER_DOWN或者ACTION_POINTER_UP的case中来进行多点触控的事件处理.

多点触控操作的一般流程

public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()){
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "第一个手指按下");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "最后一个手指抬起");
                break;
            case MotionEvent.ACTION_MOVE:
                for(int i=0;i<event.getPointerCount();i++){
                    Log.i(TAG,"第"+(event.getPointerId(i))+"个手指移动");
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                Log.i(TAG, "第"+(event.getActionIndex()+1)+"个手指按下");
                break;
            case MotionEvent.ACTION_POINTER_UP:
                Log.i(TAG, "第"+(event.getActionIndex()+1)+"个手指松开");
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.i(TAG, "第" + (event.getActionIndex() + 1) + "个手指CANCEL");
                break;
            case MotionEvent.ACTION_OUTSIDE:
                Log.i(TAG, "第" + (event.getActionIndex() + 1) + "个手指OUTSIDE");
                break;
        }
        return true;
    }

干货

getAction() 与 getActionMasked()讲解 重要

  • 手指索引和事件的合并原因 一般来说我们可以通过为事件添加一个int类型的index属性来区分,(但是我们知道谷歌工程师是有洁癖的,为了添加一个通常数值不会超过10的index属性就浪费一个int大小的空间简直是不能忍受的,于是工程师们将这个index属性和事件类型直接合并了) 前面这句话是从其他道友文章里面复制过来的-谢谢。所以说,当发生多个手指点击屏幕时,我们从系统获得的action其实是压缩后的值.我们需要把他正确的拿出来.
  • 从合并后的值中取出手指索引和事件类型 我们知道,手指索引和事件类型都被压缩到一个int类型的值里面了.int类型共32位(0x00000000),他们用 最低8位(0x000000ff) 表示事件类型,再往 前的8位(0x0000ff00) 表示事件编号.下面我们以手指按下为例子整理了一些事件的数值.
ACTION_DOWN 的默认数值为 (0x00000000)
ACTION_POINTER_DOWN 的默认数值为 (0x00000005)
手指按下触发的发事件(及数值)
第1个手指按下ACTION_DOWN (0x00000000)
第2个手指按下ACTION_POINTER_DOWN (0x00000105)
第3个手指按下ACTION_POINTER_DOWN (0x00000205)
第4个手指按下ACTION_POINTER_DOWN (0x00000305)
  1. 从上面表格中我们看加粗数字部分可以明显看到,当手指按下时他们的顺序是一次增加的.这两位就是我们的索引位.而且此索引位明显是支持超过4个手指按下事件的.
  2. 最后两位则是事件类型的标识,他代表了不同事件的类型.

结论

  1. 当有多点触控时,我们必须用getActionMasked()来获取事件类型.
  2. 单点触控时,因为按下数值的变化,所以我们可以用getAction()或者getActionMasked()来获取事件的类型

题外 getActionMask()是如何从事件原始数值中取出类型的.

actionIndex

  • actionIndex 是手指按下时的一串事件序列,标识的是第几根手指按下.在int中用0x00000000这两位来表示.
  • 使用 getActionIndex() 可以获取到这个index数值。getActionIndex() 只在 down 和 up 时有效,move 时是无效的。因为无论哪个手指移动,我们用event.getActionIndex()获得的值都是0.??为什么呢? 源码:
public final int getActionIndex() {
    return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT;
}

经过源码我们可以看出,getActionIndex方法返回的是 nativeGetAction(ptr) 与 ACTION_POINTER_INDEX_MASK按位与之后再向右移动个位算出来的.nativeGetAction(ptr)始终返回0x00000002,经过此次位运算后,所以结果为0.

  • actionIndex 代表手指按下时的编号,但是他不是唯一不变的. 比如按下时手指顺序是第一个手指按下>第二个手指按下>第三个手指按下>第四个手指按下,如果抬起时的顺序为第二根手指>第三根手指>四根手指>第一根手指的话,那么actionIndex的顺序会如下图所示:

因为抬起时第二根手指先抬起来,所以actionIndex为2,而当前面的手指抬起时后面的actionIndex顺序依次减一所以第三根手指抬起时actionIndex依旧是2,第四根手指抬起时,还是2.这就是actionIndex的一个变动规律.

PointId

经上面actionIndex的知识我们可以知道,actionIndex并不是我们跟踪某一个手指的依据,因为在不同的手指按下或抬起时,他会动态变化.那么如果跟踪一个手指从按下到抬起的一个完整过程呢?官方给我们提供了一个新的变量pointId

  • pointId在手指按下时产生,手指抬起或者事件被取消后消失,是一个事件流程中唯一不变的标识,可以在手指按下时 通过 getPointerId(int pointerIndex) 获得。pointerIndext就是actionIndex
  • 追踪事件流,请认准 PointId,这是唯一官方指定标准,不要使用 ActionIndex

历史数据

由于我们的设备非常灵敏,手指稍微移动一下就会产生一个移动事件,所以移动事件会产生的特别频繁,为了提高效率,系统会将近期的多个移动事件(move)按照事件发生的顺序进行排序打包放在同一个 MotionEvent 中,与之对应的产生了以下方法:

事件
简介
getHistorySize()获取历史事件集合大小
getHistoricalX(int pos)获取第pos个历史事件x坐标(pos < getHistorySize())
getHistoricalY(int pos)获取第pos个历史事件y坐标(pos < getHistorySize())
getHistoricalX (int pointerIndex, int pos)获取第pin个手指的第pos个历史事件x坐标(pointerIndex < getPointerCount(), pos < getHistorySize() )
getHistoricalY (int pointerIndex, int pos)获取第pin个手指的第pos个历史事件y坐标(pointerIndex < getPointerCount(), pos < getHistorySize() )

注意:

  1. 历史数据只有 ACTION_MOVE 事件。
  2. 历史数据单点触控和多点触控均可以用。 官方文档例子:按时间顺序消耗掉所有指针的例子
 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));
     }
 }

这玩意官方源码里有使用到,我暂时还没想我们在实际开发中哪里能用到.

获取事件发生的时间

方法
简介
getDownTime()获取手指按下的时间
getEventTime()获取当前事件发生的时间。
getHistoricalEventTime(int pos) pos<getHistorySize()获取历史事件发生的时间

这几个方法返回值为long类型,单位都是毫秒

获取压力(接触面积大小)

MotionEvent支持获取某些输入设备(手指或触控笔)的与屏幕的接触面积和压力大小,主要有以下方法:

方法
简介
getSize()获取第1个手指与屏幕接触面积的大小
getSize(int pointerIndex)获取第pointerIndex个手指与屏幕接触面积的大小
getHistoricalSize(int pos)获取历史数据中第1个手指在第pos次事件中的接触面积
getHistoricalSize(int pointerIndex, int pos)获取历史数据中第pointerIndex个手指在第pos次事件中的接触面积
getPressure()获取第一个手指的压力大小
getPressure(int pointerIndex)获取第pointerIndex个手指的压力大小
getHistoricalPressure(int pos)获取历史数据中第1个手指在第pos次事件中的压力大小
getHistoricalPressure(int pointerIndex, int pos)获取历史数据中第pointerIndex个手指在第pos次事件中的压力大小

1.描述中使用了手指,触控笔也是一样的。 2. pointerIndex<getPointerCount()

注意:

  1. 获取接触面积大小和获取压力大小是需要硬件支持的。
  2. 非常不幸的是大部分设备所使用的电容屏不支持压力检测,但能够大致检测出接触面积。
  3. 大部分设备的 getPressure() 是使用接触面积来模拟的。
  4. 由于某些未知的原因(可能系统版本和硬件问题),某些设备不支持该方法 我用不同的设备对这两个方法进行了测试,然而不同设备测试出来的结果不相同,之后经过我多方查证,发现是系统问题,有的设备上只有 getSize() 能用,有的设备上只有 getPressure() 能用,而有的则两个都不能用。

由于获取接触面积和获取压力大小受系统和硬件影响,使用的时候一定要进行数据检测,以防因为设备问题而导致程序出错。

鼠标事件

由于触控笔事件和手指事件处理流程大致相同,所以就不讲解了,这里讲解一下与鼠标相关的几个事件:

事件
简介
ACTION_HOVER_ENTER指针移入到窗口或者View区域,但没有按下。
ACTION_HOVER_MOVE指针在窗口或者View区域移动,但没有按下。
ACTION_HOVER_EXIT指针移出到窗口或者View区域,但没有按下。
ACTION_SCROLL滚轮滚动,可以触发水平滚动(AXIS_HSCROLL)或者垂直滚动(AXIS_VSCROLL)

注意:

  1. 这些事件类型是 安卓4.0 (API 14) 才添加的。
  2. 使用 getActionMasked() 获得这些事件类型。
  3. 这些事件不会传递到 onTouchEvent(MotionEvent) 而是传递到 onGenericMotionEvent(MotionEvent) 。

输入设备类型判断

输入设备类型判断也是安卓4.0 (API 14) 才添加的,主要包括以下几种设备:

设备类型
简介
TOOL_TYPE_ERASER橡皮擦
TOOL_TYPE_FINGER手指
TOOL_TYPE_MOUSE鼠标
TOOL_TYPE_STYLUS手写笔
TOOL_TYPE_UNKNOWN未知类型

使用 getToolType(int pointerIndex) 来获取对应的输入设备类型,pointIndex可以为0,但必须小于 getPointerCount()。

工具

在线进制转换

问题

在高版本中,我们用getAction()而不用getActionMasked()会出现什么问题吗?