安卓自定义 View 进阶:MotionEvent 详解

Android MotionEvent 详解,之前用了两篇文章 事件分发机制原理事件分发机制详解来讲解事件分发,而作为事件分发主角之一的 MotionEvent 并没有过多的说明,本文就带大家了解 MotionEvent 的相关内容,简要介绍触摸事件,主要包括 单点触控、多点触控、鼠标事件 以及 getAction() 和 getActionMasked() 的区别。

Android 将所有的输入事件都放在了 MotionEvent 中,随着安卓的不断发展壮大,MotionEvent 也开始变得越来越复杂,下面是我自己整理的 MotionEvent 大事记:

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

以上仅仅是简要的说明几次比较大的变动,细小的修复和更新不计其数,此处就不一一列出了,反正也没人关心这些东西。
MotionEvent 负责集中处理所有类型设备的输入事件,但是由于某些设备使用的几率较小本文会忽略讲解,或者简要讲解,例如:
1、轨迹球只出现在最早的设备上,现代的设备上已经见不到了,本文不再叙述。
2、触控笔和手指处理流程基本相同,不再多说。
3、鼠标在手机上使用概率也比较小,会在文末简要介绍。

单点触控

单点触控就非常简单啦,入门的工程师都会用,上一篇文章也简要介绍过,主要涉及以下几个事件:

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

和以下的几个方法:

方法 简介
getAction() 获取事件类型。
getX() 获得触摸点在当前 View 的 X 轴坐标。
getY() 获得触摸点在当前 View 的 Y 轴坐标。
getRawX() 获得触摸点在整个屏幕的 X 轴坐标。
getRawY() 获得触摸点在整个屏幕的 Y 轴坐标。

关于 getgetRaw 的区别可以参考这一篇文章 安卓自定义View基础-坐标系
单点触控一次简单的交互流程是这样的:

手指落下(ACTION_DOWN) -> 多次移动(ACTION_MOVE) -> 离开(ACTION_UP)

  • 本次事例中 ACTION_MOVE 有多次触发。
  • 如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。

单点触摸事件流程

针对单点触控的事件处理一般是这样写的:

  • @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);
  • }

相信小伙伴对此已经非常熟悉了,经常使用的东西,我也不啰嗦了。

但其中有两个比较特殊的事件: ACTION_CANCELACTION_OUTSIDE
为什么说特殊呢,因为它们是由程序触发而产生的,而且触发条件也非常特殊,通常情况下即便不处理这两个事件也没有什么问题。接下来我们就扒一扒它们的真面目:

ACTION_CANCEL

ACTION_CANCEL 的触发条件是事件被上层拦截,然而我们在 [事件分发机制原理][customview/dispatch-touchevent-theory] 一文中了解到当事件被上层 View 拦截的时候,ChildView 是收不到任何事件的,ChildView 收不到任何事件,自然也不会收到 ACTION_CANCEL 了,所以说这个 ACTION_CANCEL 的正确触发条件并不是这样,那么是什么呢?

事实上,只有上层 View 回收事件处理权的时候,ChildView 才会收到一个 ACTION_CANCEL 事件。

这样说可能不太容易理解,咱举个例子?

例如:上层 View 是一个 RecyclerView,它收到了一个 ACTION_DOWN 事件,由于这个可能是点击事件,所以它先传递给对应 ItemView,询问 ItemView 是否需要这个事件,然而接下来又传递过来了一个 ACTION_MOVE 事件,且移动的方向和 RecyclerView 的可滑动方向一致,所以 RecyclerView 判断这个事件是滚动事件,于是要收回事件处理权,这时候对应的 ItemView 会收到一个 ACTION_CANCEL ,并且不会再收到后续事件。

通俗一点?

  • RecyclerView:儿砸,这里有一个 `ACTION_DOWN` 你看你要不要。
  • ItemView :好嘞,我看看。
  • RecyclerView:噫?居然是移动事件`ACTION_MOVE`,我要滚起来了,儿砸,我可能要把你送去你姑父家(缓存区)了,在这之前给你一个 `ACTION_CANCEL`,你要收好啊。
  • ItemView :…...

这是实际开发中最有可能见到 ACTION_CANCEL 的场景了。

ACTION_OUTSIDE

ACTION_OUTSIDE的触发条件更加奇葩,从字面上看,outside 意思不就是超出区域么?然而不论你如何滑动超出控件区域都不会触发 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.

一个触摸事件已经发生了UI元素的正常范围之外。因此不再提供完整的手势,只提供 运动/触摸 的初始位置。

我们知道,正常情况下,如果初始点击位置在该视图区域之外,该视图根本不可能会收到事件,然而,万事万物都不是绝对的,肯定还有一些特殊情况,你可曾还记得点击 Dialog 区域外关闭吗?Dialog 就是一个特殊的视图(没有占满屏幕大小的窗口),能够接收到视图区域外的事件(虽然在通常情况下你根本用不到这个事件),除了 Dialog 之外,你最可能看到这个事件的场景是悬浮窗,当然啦,想要接收到视图之外的事件需要一些特殊的设置。

设置视图的 WindowManager 布局参数的 flags为FLAG_WATCH_OUTSIDE_TOUCH,这样点击事件发生在这个视图之外时,该视图就可以接收到一个 ACTION_OUTSIDE 事件。

参见StackOverflow:How to dismiss the dialog with click on outside of the dialog?

由于这个事件用到的几率比较小,此处就不展开叙述了,以后用到的时候再详细讲解。

多点触控

Android 在 2.2 版本的时候开始支持多点触控,一旦出现了多点触控,很多东西就突然之间变得麻烦起来了,首先要解决的问题就是 多个手指同时按在屏幕上,会产生很多的事件,这些事件该如何区分呢?

为了区分这些事件,工程师们用了一个很简单的办法--编号,当手指第一次按下时产生一个唯一的号码,手指抬起或者事件被拦截就回收编号,就这么简单。

第一次按下的手指特殊处理作为主指针,之后按下的手指作为辅助指针,然后随之衍生出来了以下事件(注意增加的事件和事件简介的变化):

事件 简介
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坐标

由于多点触控部分涉及内容比较多,也很复杂,我准备单独用一篇文章进行详细叙述,所以这里只叙述一些基础的内容作为铺垫:

getAction() 与 getActionMasked()

当多个手指在屏幕上按下的时候,会产生大量的事件,如何在获取事件类型的同时区分这些事件就是一个大问题了。

一般来说我们可以通过为事件添加一个int类型的index属性来区分,但是我们知道谷歌工程师是有洁癖的(在 [自定义View分类与流程][customview/CustomViewProcess] 的onMeasure中已经见识过了),为了添加一个通常数值不会超过10的index属性就浪费一个int大小的空间简直是不能忍受的,于是工程师们将这个index属性和事件类型直接合并了。

top Created with Sketch.