写点什么

Android 开发时的多点触控是如何实现的?

发布于: 2021 年 01 月 13 日
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。

对多点触控消息进行处理的代码如下:

 1String TAG = "qijian"; 2@Override 3public boolean onTouchEvent(MotionEvent event) { 4    switch (event.getActionMasked()) { 5    case MotionEvent.ACTION_DOWN: 6        Log.e(TAG,"第1根手指按下"); 7        break; 8    case MotionEvent.ACTION_UP: 9        Log.e(TAG,"最后一根手指抬起");10        break;11    case MotionEvent.ACTION_POINTER_DOWN:12        Log.e(TAG,"又一根手指按下");13        break;14    case MotionEvent.ACTION_POINTER_UP:15        Log.e(TAG,"又一根手指抬起");16        break;17    }18    return true;19}20...21    }
复制代码

这里仅列出了手指按下和手指抬起所触发的消息类型,而在手指移动时,无论是单点触控还是多点触控,所触发的消息都是 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:

用于获取用户按下的手指个数,一般我们用它来遍历屏幕上的所有手指,遍历手指的代码如下:

1for (int i = 0; i < event.getPointerCount(); i++) {2    int pointerId = event.getPointerId(i);3}
复制代码

前面讲过,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,其中的初始化代码如下:

 1public class MultiTouchView extends View { 2    // 用于判断第2根手指是否存在 3    private boolean haveSecondPoint = false; 4    // 记录第2根手指的位置 5    private PointF point = new PointF(0, 0); 6    private Paint mDefaultPaint = new Paint(); 7 8    public MultiTouchView(Context context) { 9        super(context);10        init();11    }1213    public MultiTouchView(Context context, @Nullable AttributeSet attrs) {14        super(context, attrs);15        init();16    }1718    public MultiTouchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {19        super(context, attrs, defStyleAttr);20        init();21    }2223    private void init() {24        mDefaultPaint.setColor(Color.WHITE);25        mDefaultPaint.setAntiAlias(true);26        mDefaultPaint.setTextAlign(Paint.Align.CENTER);27        mDefaultPaint.setTextSize(30);28    }29}
复制代码

这样我们就自定义了一个 View,很明显它内部不会再包裹其他的 View 控件,所以继承自 View 类即可。

我们定义了 3 个变量,其中:

  • haveSecondPoint 用于判断第 2 根手指是否按下。

  • point 用于记录第 2 根手指的位置。

  • mDefaultPaint 是画笔变量,用于画第 2 根手指位置处的白色圆圈。

2

onTouchEvent

然后,在用户按下手指时,需要加以判断,当前是第几根手指,然后获取第 2 根手指的位置,下面列出完整代码:

 1public boolean onTouchEvent(MotionEvent event) { 2    int index = event.getActionIndex(); 3 4    switch (event.getActionMasked()) { 5    case MotionEvent.ACTION_POINTER_DOWN: 6        if (event.getPointerId(index) == 1) { 7            haveSecondPoint = true; 8            point.set(event.getX(), event.getY()); 9        }10        break;11    case MotionEvent.ACTION_MOVE:12        try {13            if (haveSecondPoint) {14                int pointerIndex = event.findPointerIndex(1);15                point.set(event.getX(pointerIndex), event.getY(pointerIndex));16            }17        } catch (Exception e) {18            haveSecondPoint = false;19        }20        break;21    case MotionEvent.ACTION_POINTER_UP:22        if (event.getPointerId(index) == 1) {23            haveSecondPoint = false;24        }25        break;26    case MotionEvent.ACTION_UP:27        haveSecondPoint = false;28        break;29    }3031    invalidate();32    return true;33}
复制代码

获取当前活动手指的 PointerIndex 值:

1int index = event.getActionIndex();
复制代码

我们知道,当第 1 根手指按下的时候触发的是 ACTION_DOWN 消息,随后的手指按下的时候触发的都是 ACTION_POINTER_DOWN 消息。因为我们要跟踪第 2 根手指,所以这里只需要识别 ACTION_POINTER_DOWN 消息即可:

1case MotionEvent.ACTION_POINTER_DOWN:2    if (event.getPointerId(index) == 1) {3        haveSecondPoint = true;4        point.set(event.getX(), event.getY());5    }6    break;
复制代码

我们也知道 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 消息:

 1case MotionEvent.ACTION_MOVE: 2    try { 3        if (haveSecondPoint) { 4            int pointerIndex = event.findPointerIndex(1); 5            point.set(event.getX(pointerIndex), event.getY(pointerIndex)); 6        } 7    } catch (Exception e) { 8        haveSecondPoint = false; 9    }10    break;
复制代码

需要注意,因为这里使用 event.findPointerIndex(1)来强制获取 PointerId 为 1 的手指 PointerIndex,在异常情况下可能出现越界,所以使用 try…catch…来进行保护。

在这里,我们使用 event.getX(pointerIndex)来获取指定手指的位置信息。同样地,这个问题也放在后面讲解。

当手指抬起时,会触发 ACTION_POINTER_UP 消息:

1case MotionEvent.ACTION_POINTER_UP:2    if (event.getPointerId(index) == 1) {3        haveSecondPoint = false;4    }5    break;
复制代码

同样地,使用 event.getPointerId(index)来获取当前抬起手指的 PointerId,如果是 1,那就说明是第 2 根手指抬起了,这时就把 haveSecondPoint 设为 false。

当全部手指抬起时,会触发 ACTION_UP 消息:

1case MotionEvent.ACTION_UP:2    haveSecondPoint = false;3    break;
复制代码

在最后一根手指抬起时,把 haveSecondPoint 设为 false,白色圆圈从屏幕上消失。

最后,调用 invalidate();来重绘界面。

3

onDraw

在重绘界面时,主要是在 point 中存储的第 2 根手指的位置处画一个白色圆圈:

 1protected void onDraw(Canvas canvas) { 2 3    canvas.drawColor(Color.GREEN); 4    if (haveSecondPoint) { 5        canvas.drawCircle(point.x, point.y, 50, mDefaultPaint); 6    } 7 8    canvas.save(); 9    canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);10    canvas.drawText("追踪第2个按下手指的位置", 0, 0, mDefaultPaint);11    canvas.restore();12}
复制代码

首先,为整个屏幕绘一层绿色,把上一屏的内容清掉:

1canvas.drawColor(Color.GREEN);
复制代码

然后,如果第 2 根手指按下了,则在它的位置处画一个圆圈:

1if (haveSecondPoint) {2    canvas.drawCircle(point.x, point.y, 50, mDefaultPaint);3}
复制代码

最后,在布局的中间位置写上提示文字:

1canvas.save();2canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);3canvas.drawText("追踪第2个按下手指的位置", 0, 0, mDefaultPaint);4canvas.restore();
复制代码

有关 Canvas 的操作及写字的操作,在《Android 自定义控件开发入门与实战》一书中有详细章节讲述,这里就不再赘述了。

在写好控件以后,直接利用 XML 引入布局即可,这里不再展示,效果就是我们想要的样子。

关于作者

启舰

本名张恩伟,Android 研发专家、CSDN 博客专家、CSDN 博客之星,《Android 自定义控件入门与实战》《Android 自定义控件高级进阶与精彩实例》作者,电子工业出版社博文视点优秀作者,曾就职于阿里巴巴,现就职于 vivo。

图书推荐


▊《Android 自定义控件高级进阶与精彩实例》

启舰 著

  • 专注于介绍 Android 自定义控件进阶知识

  • 通过精彩的案例对各种绘制、动画技术进行了糅合讲解

本书主要内容有 3D 特效的实现、高级矩阵知识、消息处理机制、派生类型的选择方法、多点触控及辅助类、RecyclerView 的使用方法及 3D 卡片的实现、动画框架 Lottie 的讲解与实战等。

读者可以通过本书从宏观层面、源码层面对 Android 自定义控件建立完整的认识。

用户头像

还未添加个人签名 2019.10.21 加入

还未添加个人简介

评论

发布
暂无评论
Android开发时的多点触控是如何实现的?