前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android开发时的多点触控是如何实现的?

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

作者头像
博文视点Broadview
发布2021-01-14 10:09:44
9170
发布2021-01-14 10:09:44
举报

对于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。

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

代码语言:javascript
复制
String TAG = "qijian";
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getActionMasked()) {
    case MotionEvent.ACTION_DOWN:
        Log.e(TAG,"第1根手指按下");
        break;
    case MotionEvent.ACTION_UP:
        Log.e(TAG,"最后一根手指抬起");
        break;
    case MotionEvent.ACTION_POINTER_DOWN:
        Log.e(TAG,"又一根手指按下");
        break;
    case MotionEvent.ACTION_POINTER_UP:
        Log.e(TAG,"又一根手指抬起");
        break;
    }
    return true;
}
...
    }

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

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

代码语言:javascript
复制
for (int i = 0; i < event.getPointerCount(); i++) {
    int pointerId = event.getPointerId(i);
}

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

代码语言:javascript
复制
public class MultiTouchView extends View {
    // 用于判断第2根手指是否存在
    private boolean haveSecondPoint = false;
    // 记录第2根手指的位置
    private PointF point = new PointF(0, 0);
    private Paint mDefaultPaint = new Paint();

    public MultiTouchView(Context context) {
        super(context);
        init();
    }

    public MultiTouchView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MultiTouchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mDefaultPaint.setColor(Color.WHITE);
        mDefaultPaint.setAntiAlias(true);
        mDefaultPaint.setTextAlign(Paint.Align.CENTER);
        mDefaultPaint.setTextSize(30);
    }
}

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

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

  • haveSecondPoint用于判断第2根手指是否按下。
  • point用于记录第2根手指的位置。
  • mDefaultPaint是画笔变量,用于画第2根手指位置处的白色圆圈。

2

onTouchEvent

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

代码语言:javascript
复制
public boolean onTouchEvent(MotionEvent event) {
    int index = event.getActionIndex();

    switch (event.getActionMasked()) {
    case MotionEvent.ACTION_POINTER_DOWN:
        if (event.getPointerId(index) == 1) {
            haveSecondPoint = true;
            point.set(event.getX(), event.getY());
        }
        break;
    case MotionEvent.ACTION_MOVE:
        try {
            if (haveSecondPoint) {
                int pointerIndex = event.findPointerIndex(1);
                point.set(event.getX(pointerIndex), event.getY(pointerIndex));
            }
        } catch (Exception e) {
            haveSecondPoint = false;
        }
        break;
    case MotionEvent.ACTION_POINTER_UP:
        if (event.getPointerId(index) == 1) {
            haveSecondPoint = false;
        }
        break;
    case MotionEvent.ACTION_UP:
        haveSecondPoint = false;
        break;
    }

    invalidate();
    return true;
}

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

代码语言:javascript
复制
int index = event.getActionIndex();

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

代码语言:javascript
复制
case MotionEvent.ACTION_POINTER_DOWN:
    if (event.getPointerId(index) == 1) {
        haveSecondPoint = true;
        point.set(event.getX(), event.getY());
    }
    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消息:

代码语言:javascript
复制
case MotionEvent.ACTION_MOVE:
    try {
        if (haveSecondPoint) {
            int pointerIndex = event.findPointerIndex(1);
            point.set(event.getX(pointerIndex), event.getY(pointerIndex));
        }
    } catch (Exception e) {
        haveSecondPoint = false;
    }
    break;

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

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

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

代码语言:javascript
复制
case MotionEvent.ACTION_POINTER_UP:
    if (event.getPointerId(index) == 1) {
        haveSecondPoint = false;
    }
    break;

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

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

代码语言:javascript
复制
case MotionEvent.ACTION_UP:
    haveSecondPoint = false;
    break;

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

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

3

onDraw

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

代码语言:javascript
复制
protected void onDraw(Canvas canvas) {

    canvas.drawColor(Color.GREEN);
    if (haveSecondPoint) {
        canvas.drawCircle(point.x, point.y, 50, mDefaultPaint);
    }

    canvas.save();
    canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);
    canvas.drawText("追踪第2个按下手指的位置", 0, 0, mDefaultPaint);
    canvas.restore();
}

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

代码语言:javascript
复制
canvas.drawColor(Color.GREEN);

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

代码语言:javascript
复制
if (haveSecondPoint) {
    canvas.drawCircle(point.x, point.y, 50, mDefaultPaint);
}

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

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

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

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

关于作者

启舰

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-01-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 博文视点Broadview 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档