安卓入门三十七 特殊控件的事件处理方案

世界上并没有绝对完美的东西,当”事件处理”遇上”自定义View”,一场好戏就开演了。

特殊形状控件

在通常的情况下,自定义 View 直接使用系统的事件体系处理就行,我们也不需要特殊处理,然而当一些特殊的控件出现的时候,麻烦就来了,举个栗子:

这是一个在遥控器上非常常见的按键布局,注意中间上下左右选择的部分,看起来十分简单,然而当你真正准备在手机上实现的时候麻烦就出现了。因为所有的 View 默认都是矩形的,所以事件接收区域也是矩形的,如果直接使用系统提供的 View 来组合出一摸一样的布局也很简单,但点击区域该如何处理?显然有部分点击区域是在控件外面的,并且会产生重叠区域:

红色方框表示 View 的可点击区域。

当我们面对这样比较奇特的控件的时候,有很多处理办法,比较投机的一种就是背景贴一个静态图,按钮做成透明的,设置小一点,放在对应的位置,这样可以保证不会误触,当然了如果想要点击效果可以在按钮按下的时候更新一下背景图,这样虽然也可以,但是这样会导致可点击区域变小,体验效果变差,设计方案变得复杂,而且逻辑也不容易处理,是一种非常糟糕的设计。

当然了,看了我这么多文章的小伙伴应该也猜到接下来要说什么了,没错,就是自定义 View。当我们面对一些奇葩控件的时候,自定义 View 就变成了一种非常好用的处理方案。

注意:

本文中所有的 自定义View 均继承自 CustomView ,这是一个自定义的超类,目的是简化 自定义View 部分常用操作,你可以在 ViewSupport 中找到它以及关于它的简介。
警告:测试本文章示例之前请关闭硬件加速。

特殊形状控的点击区域判断

要进行特殊形状的点击判断,要用到一个之前没有使用过的类:Region。

Region 直接翻译的意思是 地域,区域。在此处应该是区域的意思。它和 Path 有些类似,但 Path 可以是不封闭图形,而 Region 总是封闭的。可以通过 setPath 方法将 Path 转换为 Region。

本文中我们重点要使用到的是 Region 中的 contains 方法,这个方法可以判断一个点是否包含在该区域内。

接下来是一个简单的示例,判断手指是否是在圆形区域内按下:

就是创建了个 Path 并在其中添加圆形,之后将 Path 设置到 Region 中,当手指在屏幕上按下的时候判断手指位置是否在 Region 区域内。

画布变换后坐标转换问题

还是本文一开始的例子,绘制一个上下左右选择按键,这个控件是上下左右对称的,熟悉我代码风格的小伙伴都知道,如果遇上这种问题,我肯定是要将坐标系平移到这个控件中心的,这样数据比较好计算,然而进行画布变换操作会产生一个新问题:手指触摸的坐标系和画布坐标系不统一,就可能引起手指触摸位置和绘制位置不统一。

举个栗子:

画布移动后在手指按下位置绘制一个圆,可以看到,直接拿手指触摸位置的坐标来绘制会导致绘制位置不正确,两者坐标是相同的,但是由于坐标系不同,导致实际显示位置不同。

接下来就对上面的示例进行简单的改造一下,让触摸位置和实际绘制绘制重合。小白点和黑色的圆没有完全重合是因为系统显示触摸位置的绘制逻辑和我使用的绘制逻辑不太相同导致的。

  1. 使用全局坐标系
  2. 使用逆矩阵的 mapPoints

原理嘛,其实非常简单,我们在画布上正常的绘制,需要将画布坐标系转换为全局坐标系后才能真正的绘制内容。所以我们反着来,将获得到的全局坐标系坐标使用当前画布的逆矩阵转化一下,就转化为当前画布的坐标系坐标了,如果对 Matrix原理 和 Matrix详解 理解了,即便我不说你们也肯定会想到这个方案的。

仿遥控器按钮代码示例

在解决了上述两大难题之后,相信不论形状如何奇葩的自定义控件,基本上都难不倒大家了,最后用一个简单的示例作为结尾,还是文章开头所举的例子,核心内容就是上面讲的两个东西。

运行效果:

当手指在某一区域活动时,该区域会高亮显示,如果注册了监听器,点击某一区域会触发监听器回调。

关于硬件加速的问题

硬件加速是个好东西,但是处理不好会引起诸多问题,博主为了怕麻烦我一直关闭硬件加速。

然而硬件加速在 Android 4.0 以上是默认开启的,这就导致了有好几位魔法师反馈测试结果和我的测试结果不同,我来简单说明一下硬件加速干了什么事情,以及这些文章中的锅是如何产生的,应该由谁来背。

Matrix 的作用: Matrix作用就是坐标映射。
其核心功能就是将单个 View 的坐标系转化为屏幕(物理)坐标系,虽然转换一次费不了多少时间,但是当执行动画效果等需要大量快速重绘的情况下,耗费的时间就需要考量一下了,于是乎,硬件加速干了一件非常精明的事情,把所有画布坐标系都设置为屏幕(物理)坐标系,之后在 View 绘制区域设置一个遮罩,保证绘制内容不会超过 View 自身的大小,这样就直接跳过坐标转换过程,可以节省坐标系之间数值转换耗费的时间。因此导致了以下问题:

  • 开启硬件加速情况下 event.getX() 和 不开启情况下 event.getRawX() 等价,获取到的是屏幕(物理)坐标 (本文的锅)。
  • 开启硬件加速情况下 event.getRawX() 数值是一个错误数值,因为本身就是全局的坐标又叠加了一次 View 的偏移量,所以肯定是不正确的 (本文的锅)。
  • 从 Canvas 获取到的 Matrix 是全局的,默认情况下 x,y 偏移量始终为0,因此你不能从这里拿到当前 View 的偏移量 ( Matrix系列文章中的锅 )。
  • 由于其使用的是遮罩来控制绘制区域,所以如果重绘 path 时,如果 path 区域变大,但没有执行单步操作会导致 path 绘制不完整或者看起来比较奇怪 (Path系列文章中的锅)。

个人建议:

  • APP全局关闭硬件加速。
  • 针对动画较多的 Activity 或者 View 单独开启硬件加速。
  • 如果应用要兼容到 3.0 以下,不要使用硬件加速的特性,或者进行兼容处理。
  • 如果 自定义View 出现与绘图相关的异常,请务必检查一下硬件加速。
  • 如果想关掉硬件加速看这里: Android如何关闭硬件加速 

总结

本文虽然代码比较多,但核心概念非常简单,主要涉及以下两点:

  • Region 的区域检测。
  • Matrix 的坐标映射。

 多点触控详解

多点触控 ( Multitouch,也称 Multi-touch ),即同时接受屏幕上多个点的人机交互操作,多点触控是从 Android 2.0 开始引入的功能,在 Android 2.2 时对这一部分进行了重新设计。

在本文开始之前,先回顾一下 MotionEvent详解 中提到过的内容:

  • Android 将所有的事件都封装进了 Motionvent 中。
  • 我们可以通过复写 onTouchEvent 或者设置 OnTouchListener 来获取 View 的事件。
  • 多点触控获取事件类型请使用 getActionMasked() 。
  • 追踪事件流请使用 PointId。

多点触控相关的事件:

事件

简介

ACTION_DOWN

第一个 手指 初次接触到屏幕 时触发。

ACTION_MOVE

手指 在屏幕上滑动 时触发,会多次触发。

ACTION_UP

最后一个 手指 离开屏幕 时触发。

ACTION_POINTER_DOWN

有非主要的手指按下(即按下之前已经有手指在屏幕上)。

ACTION_POINTER_UP

有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。

以下事件类型不推荐使用

---以下事件在 2.2 版本以上被标记为废弃---

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坐标

回顾完毕,开始正文。

一、多点触控相关问题

在引入多点触控之前,事件的类型很少,基本事件类型只有按下(down)、移动(move) 和 抬起(up),即便加上那些特殊的事件类型也只有几种而已,所以我们可以用几个常量来标记这些事件,在使用的时候使用 getAction() 方法来获取具体的事件,之后和这些常量进行对比就行了。

在 Android 2.0 版本的时候,开始引入多点触控技术,由于技术上并不成熟,硬件和驱动也跟不上,多数设备只能支持追踪两三个点而已,因此在设计 API 上采取了一种简单粗暴的方案,添加了几个常量用于多点触控的事件类型的判断。

事件

简介

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 个手指抬起,已废弃,不推荐使用。

在多指触控中所有的移动事件都是使用 ACTION_MOVE, 并没有追踪某一个手指的 move 事件类型,个人猜测主要是因为:很难无歧义的实现单独追踪每一个手指。

要理解这个,首先要明白设备是如何识别多点触控的,设备没有眼睛,不能像我们人一样看到有几个手指(或者触控笔)在屏幕上。
目前大多数 Android 设备都是电容屏,它们感知触摸是利用手指(触控笔)与屏幕接触产生的微小电流变化,之后通过计算这些电流变化来得出具体的触摸位置,在多点触控中,当两个触摸点足够靠近时,设备实际上是无法分清这两个点的。因此当两个触摸点靠近(重合)后再分开,设备很可能就无法正确的追踪两个点了,所以也很难实现无歧义的追踪每一个点。

并且从软件上来说,事件的编号产生和复用也是一个大问题,例如下面的场景:

事件

手指数量

编号变化

一个手指按下(命名为A)

1

A手指的编号为0,id为0

一个手指按下(命名为B)

2

B手指的编号为1,id为1

A手指抬起

1

B手指编号变更为0,id不变为1

一个手指按下(命名为C)

2

C手指编号为0,id为0,B手指编号为1,id为1

注意观察上面编号和id的变化,有两个问题,1、B手指的编号变化了。2、A手指和C手指id是相同的(A手指抬起后,C手指按下替代了A手指)。所以这就引出了一个问题:如果存在 ACTION_POINTER_X_MOVE,那么X应该用什么标志呢?编号会变化,id虽然不会变化,但id会被复用,例如A手指抬起后C手指按下,C手指复用了A手指的id。所以不论使用哪一个都不能保证唯一性。

当然了,解决问题最好的方式就是把问题抛出去,既然从硬件和软件上都不能保证唯一性和不变性,就不做区分了,因此所有的 move 事件都是 ACTION_MOVE, 具体是哪个手指产生的 move 用户可以结合其他事件(按下和抬起)来综合判断。

2.超过4个手指怎么办?

2.0 兼容版,在2.2 之前的设计中,其提供的常量最多能判断四个手指的抬起和落下,当超过四个手指时怎么办呢?

由于在 2.2 版本之前,由于没有 getActionMasked 方法,我们可以自己自己手动进行计算,例如下面这样 :

在上面的例子中有几点比较关键:

2.1、action 与 Index 的获得

我们在 MotionEvent详解 中了解过,Android中的事件一般用最后8位来表示事件类型,再往前8位来表示Index。

例如多指触控的按下事件,其事件类型是 0x00000005, 其Index标志位是 0x00000005,随着更多的手指按下,其中变化的部分是 Index 标志位,最后两位是始终不变的,所以我们只要能将这两个分离开就行了。

取得事件类型(action)

// 获取事件类型int action = event.getAction() & MotionEvent.ACTION_MASK;

这个非常简单,ACTION_MASK=0x000000ff, 与 getAction() 进行按位与操作后保留最后8位内容(十六进制每一个字符转化为二进制是4位)。

例如:
0x00000105 & 0x000000ff = 0x00000005

取得事件索引(index)

// 获取index编号int index = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;

ACTION_POINTER_INDEX_MASK = 0x0000ff00
ACTION_POINTER_INDEX_SHIFT = 8
首先让 getAction() 与 ACTION_POINTER_INDEX_MASK 按位与之后,只保留 Index 那8位,之后再右移8位,最终就拿到了 Index 的真实数值。

例如:
0x00000105 & 0x0000ff00 = 0x00000100
0x00000100 » 8 = 0x00000001

2.2、用 ACTION_POINTER_1_DOWN 代替 ACTION_POINTER_DOWN

这是因为在 2.0 版本的时候还没有 ACTION_POINTER_DOWN 的这个常量,但是它们两个点数值是相同的,都是 0x00000005,这个你可以查看官方文档或者源码,甚至你直接写 case 0x00000005 也行,抬起也是同理。

2.3、只考虑兼容 2.2 以上的版本

3. index 和 pointId 的变化规则

在 2.2 版本以上,我们可以通过 getActionIndex() 轻松获取到事件的索引(Index),但是这个事件索引的变化还是有点意思的,Index 变化有以下几个特点:

1、从 0 开始,自动增长。
2、如果之前落下的手指抬起,后面手指的 Index 会随之减小。
3、Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。
4、对 move 事件无效。

下面我们逐条解释一下具体含义。

3.1、从 0 开始,自动增长。

这一条非常简单,也很容易理解,而且在 MotionEvent详解 中讲解 getAction() 与 getActionMasked() 也简单说过。

手指按下

触发事件(数值)

第1个手指按下

ACTION_DOWN (0x00000000)

第2个手指按下

ACTION_POINTER_DOWN (0x00000105)

第3个手指按下

ACTION_POINTER_DOWN (0x00000205)

第4个手指按下

ACTION_POINTER_DOWN (0x00000305)

注意加粗的位置,数值随着手指按下而不断变大。

3.2、如果之前落下的手指抬起,后面手指的 Index 会随之减小。

这个也比较容易理解,像下面这样:

手指按下

触发事件(数值)

第1个手指按下

ACTION_DOWN (0x00000000)

第2个手指按下

ACTION_POINTER_DOWN (0x00000105)

第3个手指按下

ACTION_POINTER_DOWN (0x00000205)

第2个手指抬起

ACTION_POINTER_UP (0x00000106)

第3个手指抬起

ACTION_POINTER_UP (0x00000106)

注意最后两次触发的事件,它的 Index 都是 1,这样也比较容易解释,当原本的第 2 个手指抬起后,屏幕上就只剩下两个手指了,之前的第 3 个手指就变成了第 2 个,于是抬起时触发事件的 Index 为 1,即之前落下的手指抬起,后面手指的 Index 会随之减小。

3.3、Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。

这个就有点神奇了,通过上一条规则,我们知道,某一个手指的 Index 可能会随着其他手指的抬起而变小,这次我们用 4 个手指测试一下 Index 的变化趋势。

手指按下

触发事件(数值)

第1个手指按下

ACTION_DOWN (0x00000000)

第2个手指按下

ACTION_POINTER_DOWN (0x00000105)

第3个手指按下

ACTION_POINTER_DOWN (0x00000205)

第2个手指抬起

ACTION_POINTER_UP (0x00000106)

第3个手指抬起

ACTION_POINTER_UP (0x00000106)

第4个手指按下

ACTION_POINTER_DOWN (0x00000105)

第3个手指抬起

ACTION_POINTER_UP (0x00000206)

这个要和上一个对比这看,重点观察第 3 个手指所触发事件区别,在上一个示例中,随着第 2 个手指的抬起,第 3 个手指变化为第 2(01) 个,所以抬起时触发的是第 2 根手指的抬起事件(删除线部分)。

但是,如果第 2 个手指抬起后,落在屏幕上另外一个手指会怎样?经过测试,发现另外落下的手指会替代之前第 2 个手指的位置,系统判定为 2(01),而不是顺延下去变成 3(02),并且原本第3个手指的index变为原来数值(02),但是如果继续落下其他的手指,数值则会顺延。

即手指抬起时的 Index 会趋向于和按下时相同,虽然在手指数量不足时,Index 会变小,但是当手指变多时,Index 会趋向于保持和按下时一样。

PS:由于程序是从0开始计数的,所以 0 就是 1, 1 就是 2 …

3.4、对 move 事件无效。

这个也比较容易理解,我们所取得的 Index 属性实际上是从事件上分离下来的,但是 move 事件始终为 0x00000002,也就是说,在 move 时不论你移动哪个手指,使用 getActionIndex() 获取到的始终是数值 0。

既然 move 事件无法用事件索引(Index)区别,那么该如何区分 move 是那个手指发出的呢?这就要用到 pointId 了,pointId 和 index 最大的区别就是 pointId 是不变的,始终为第一次落下时生成的数值,不会受到其他手指抬起和落下的影响。

3.5、pointId 与 index 的异同。

相同点

不同点

1. 从 0 开始,自动增长。
2. 落下手指时优先填补空缺(填补之前抬起手指的编号)。

1. Index 会变化,pointId 始终不变。

4. Move 相关事件

4.1 actionIndex 与 pointerIndex

在 move 中无法取得 actionIndex 的,我们需要使用 pointerIndex 来获取更多的信息,例如某个手指的坐标:

getX(int pointerIndex)getY(int pointerIndex)

但是这个 pointerIndex 又是什么呢?和 actionIndex 有区别么?

实际上这个 pointerIndex 和 actionIndex 区别并不大,两者的数值是相同的,你可以认为 pointerIndex 是特地为 move 事件准备的 actionIndex。

4.2 pointerIndex 与 pointerId

类型

简介

pointerIndex

用于获取具体事件,可能会随着其他手指的抬起和落下而变化

pointerId

用于识别手指,手指按下时产生,手指抬起时回收,期间始终不变

这两个数值使用以下两个方法相互转换。

方法

简介

getPointerId(int pointerIndex)

获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。

findPointerIndex(int pointerId)

通过 pointerId 获取到当前状态下 pointIndex,之后通过 pointIndex 获取其他内容。

通常情况下,pointerIndex 和 pointerId 是相同的,但也可能会因为某些手指的抬起而变得不同。

4.3 遍历多点触控

先来一个简单的,遍历出多个手指的 move 事件:

通过遍历 pointerCount 获取到所有的 pointerIndex,同时通过 pointerIndex 来获取 pointerId,可以通过不同手指抬起和按下后移动来观察 pointerIndex 和 pointerId 的变化。

4.4 在多点触控中追踪单个手指

要实现追踪单个手指还是有些麻烦的,需要同时使用上 actionIndex, pointerId 和 pointerIndex,例如,我们只追踪第2个手指,并画出其位置

二、如何使用多点触控

多点触控应用还是比较广泛的,至少目前大部分的图片查看都需要用到多点触控技术(用于拖动和缩放图片)。

但是在某些看似不需要多触控的地方也需要对多点触控进行判断,只要是多点触控可能引起错误的地方都应该加上多点触控的判断。例如使用到 move 事件的时候,由于 move 事件可能由多个手指同时触发,所以可能会出现同时被多个手指控制的情况,如果不适当的处理,这个 move 就可能由任何一个手指触发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值