Android 开发时的多点触控是如何实现的?
对于 Android 自定义控件开发,多点触控是一个必须要懂的知识点。因为在正常的情况下操作正常的控件,使用多指操作时,基本上都会出现问题。当需要对多指操作进行兼容时,就需要这方面的知识了。
本文选自《Android 自定义控件高级进阶与精彩实例》一书,带你了解多点触控的基本知识。
—— 正文 ——
假如,我们做了这么一个功能,图像跟随手指移动。
在单指操作下,图像的移动非常流畅、正确,而如果我们使用两根手指的话,就会出现下面这种情况。
从效果图可以看出,在第 2 根手指放下,而第 1 根手指抬起时,图像会出现跳跃,直接从第 1 根手指的位置移动到了第 2 根手指的位置,这明显是不对的。这只是一个简单的例子,一般使用单指操作的控件改到多指操作的时候,都会出现问题。
这便是本文讲解多点触控的初衷。既然多点触控会造成这么多问题,那么下面就来详细了解它吧。
▼
单点触控与多点触控
1
单点触控
单点触控与多点触控是相对的,单点触控的意思是,我们只考虑一根手指的情况,而且仅处理一根手指的触摸事件,而多点触控是处理多根手指的触摸事件。
一般我们处理 MotionEvent 事件,通过 MotionEvent.getAction 来获取事件类型,这就是单点触控。在单点触控中,会涉及对下面几个消息的处理。
除了消息外,我们也经常用下面这几个函数来获取手指的位置等信息,这些函数都没有参数,也都只有在单点触控时才能使用。
对于这几个函数的使用方法,这里就不再赘述了。可以看到,我们平常所处理的 MotionEvent 事件,以及常用的 MotionEvent 函数都只是针对单点触控的,那么哪些才是多点触控的事件和函数呢?
2
多点触控
首先,多点触控的消息类型只能通过 getActionMasked 来获取。因此,判断当前代码处理的是单点触控还是多点触控,单从获取消息类型的函数就可以看出。
说明:单点触控是通过 getAction 来获取当前事件类型的,而多点触控是通过 getActionMasked 来获取的。
多点触控涉及的消息类型与单点触控的不一样,它的消息类型如下。
比如以下图中的手指按下顺序,我们来看看其中的事件触发顺序。
在效果图中,先后有 3 根手指按下,按下顺序是 1、2、3,抬起顺序是 1、3、2,而事件触发顺序如下表。
这里需要注意,
第 1 根手指按下时,收到的消息是 ACTION_DOWN;
随后的手指再按下时,收到的是 ACTION_POINTER_DOWN;
当有手指抬起时,收到的是 ACTION_POINTER_UP;
当最后一根手指抬起时,收到的是 ACTION_UP。
对多点触控消息进行处理的代码如下:
这里仅列出了手指按下和手指抬起所触发的消息类型,而在手指移动时,无论是单点触控还是多点触控,所触发的消息都是 MotionEvent.ACTION_MOVE。
在多点触控时,我们可以通过代码来获取当前移动的是哪根手指。
多点触控
1
识别按下的手指
上面讲解了在什么情况下会触发什么消息,但我们怎么来识别当前按下的是哪根手指呢?
在 MotionEvent 中有一个 Pointer 的概念:
一个 Pointer 就代表一个触摸点,每个 Pointer 都有自己的消息类型,也有自己的 X 坐标值。一个 MotionEvent 对象中可能会存储多个 Pointer 的相关信息,每个 Pointer 都有自己的 PointerIndex 和 PointerId。在多点触控中,就是用 PointerIndex 和 PointerId 来标识用户手指的。
PointerIndex 表示当前手指的索引,PointerId 是手指按下时分配的唯一 id,用来标识这根手指。
每根手指从按下、移动到离开屏幕,PointerId 是不变的,而 PointerIndex 则不是固定的。
通过下面这个例子,我们来了解一下 PointerIndex 与 PointerId 的区别。
可见同一根手指的 id 是不变的,而 PointerIndex 是会变化的,但总是以 0、1 或者 0、1、2 这样的形式出现,而不可能出现 0、2 这样间隔了一个数或者 1、2 这种没有 0 索引值的形式。
针对 PointerIndex 与 PointerId,在 MotionEvent 类中经常使用下面这几个函数。
public final int getActionIndex:
用于获取当前活动手指的 PointerIndex 值。
public final int getPointerId(int pointerIndex):
用于根据 PointerIndex 值获取手指的 PointerId,其中 pointerIndex 表示手指的 PointerIndex 值。
public final int getPointerCount:
用于获取用户按下的手指个数,一般我们用它来遍历屏幕上的所有手指,遍历手指的代码如下:
前面讲过,PointerIndex 是从 0 开始的,表示当前所有手指的索引,值从 0 到 getPointerCount() − 1,不会出现不连续的数。因此,我们通过 event.getPointerCount 可以得到当前屏幕上的手指个数。然后从 0 开始遍历 PointerIndex,同时我们还能通过 int pointerId = event.getPointerId(i)来得到每根手指 PointerIndex 所对应的 PointerId。
public final int findPointerIndex(int pointerId):
用于根据 PointerId 反向找到手指的 PointerIndex 值。
由此,我们就知道了 PointerIndex 与 PointerId 的关系,以及它们相互之间的换算方法。下面再来看看通过 PointerIndex 和 PointerId 能得到什么。
2
获取手指位置信息
通过 PointerIndex 与 PointerId,可以使用以下函数获得手指的位置信息。
public final float getX(int pointerIndex):
根据 PointerIndex 得到对应手指的 X 坐标值,该函数的意义与单点触控里的 getX 函数相同。
public final float getY(int pointerIndex):
同样地,根据 PointerIndex 得到对应手指的 Y 坐标值,该函数的意义与单点触控里的 getY 函数相同。
实例:追踪第 2 根手指
现在,我们将通过一个实例来学习上面讲到的函数。
这里实现的效果是:当用户按下第 2 根手指时,就开始追踪这根手指,无论其他手指是否抬起,只要这根手指没有抬起,就一直显示这根手指的位置,如下如。
从效果图可以看出,先后总共按下了 3 根手指,分别在左(第 1 根手指)、中(第 2 根手指)、右(第 3 根手指)。
抬起手指时,先抬起左侧第 1 根手指,然后抬起右侧第 3 根手指。可以看到,第 2 根手指的触摸点,我们使用白色圆圈显示,无论第 3 根手指是否按下,还是其他手指是否抬起,白色圆圈总是跟着第 2 根手指的移动来显示。这就实现了跟踪第 2 根手指轨迹的效果。
下面我们来看看这个效果是怎么实现的吧。
1
自定义 View 并初始化
布局很简单,就是一个全屏 View,为了在 View 上画圆圈,我们必须自定义 View,其中的初始化代码如下:
这样我们就自定义了一个 View,很明显它内部不会再包裹其他的 View 控件,所以继承自 View 类即可。
我们定义了 3 个变量,其中:
haveSecondPoint 用于判断第 2 根手指是否按下。
point 用于记录第 2 根手指的位置。
mDefaultPaint 是画笔变量,用于画第 2 根手指位置处的白色圆圈。
2
onTouchEvent
然后,在用户按下手指时,需要加以判断,当前是第几根手指,然后获取第 2 根手指的位置,下面列出完整代码:
获取当前活动手指的 PointerIndex 值:
我们知道,当第 1 根手指按下的时候触发的是 ACTION_DOWN 消息,随后的手指按下的时候触发的都是 ACTION_POINTER_DOWN 消息。因为我们要跟踪第 2 根手指,所以这里只需要识别 ACTION_POINTER_DOWN 消息即可:
我们也知道 PointerIndex 是变化的,而 PointerId 是不变的,PointerId 根据手指按下的顺序从 0 到 1 逐渐增加。因此,第 2 根手指的 PointerId 就是 1。当(event.getPointerId(index) == 1 时,就表示当前按下的是第 2 根手指,将 haveSecondPoint 设为 true,并将得到的第 2 根手指的位置设置到 point 中。
到这里,大家可能会产生疑问,上面提到的多点触控获取手指位置都用的是 event.getX(pointerIndex),而这里怎么直接用 event.getX 了呢?其实这里使用 event.getX (pointerIndex)也是可以的,大家可以先记下这个问题,后面我们再详细讲解。
当手指移动时,会触发 ACTION_MOVE 消息:
需要注意,因为这里使用 event.findPointerIndex(1)来强制获取 PointerId 为 1 的手指 PointerIndex,在异常情况下可能出现越界,所以使用 try…catch…来进行保护。
在这里,我们使用 event.getX(pointerIndex)来获取指定手指的位置信息。同样地,这个问题也放在后面讲解。
当手指抬起时,会触发 ACTION_POINTER_UP 消息:
同样地,使用 event.getPointerId(index)来获取当前抬起手指的 PointerId,如果是 1,那就说明是第 2 根手指抬起了,这时就把 haveSecondPoint 设为 false。
当全部手指抬起时,会触发 ACTION_UP 消息:
在最后一根手指抬起时,把 haveSecondPoint 设为 false,白色圆圈从屏幕上消失。
最后,调用 invalidate();来重绘界面。
3
onDraw
在重绘界面时,主要是在 point 中存储的第 2 根手指的位置处画一个白色圆圈:
首先,为整个屏幕绘一层绿色,把上一屏的内容清掉:
然后,如果第 2 根手指按下了,则在它的位置处画一个圆圈:
最后,在布局的中间位置写上提示文字:
有关 Canvas 的操作及写字的操作,在《Android 自定义控件开发入门与实战》一书中有详细章节讲述,这里就不再赘述了。
在写好控件以后,直接利用 XML 引入布局即可,这里不再展示,效果就是我们想要的样子。
关于作者
启舰
本名张恩伟,Android 研发专家、CSDN 博客专家、CSDN 博客之星,《Android 自定义控件入门与实战》《Android 自定义控件高级进阶与精彩实例》作者,电子工业出版社博文视点优秀作者,曾就职于阿里巴巴,现就职于 vivo。
图书推荐
▊《Android 自定义控件高级进阶与精彩实例》
启舰 著
专注于介绍 Android 自定义控件进阶知识
通过精彩的案例对各种绘制、动画技术进行了糅合讲解
本书主要内容有 3D 特效的实现、高级矩阵知识、消息处理机制、派生类型的选择方法、多点触控及辅助类、RecyclerView 的使用方法及 3D 卡片的实现、动画框架 Lottie 的讲解与实战等。
读者可以通过本书从宏观层面、源码层面对 Android 自定义控件建立完整的认识。
评论