对于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@Override3public 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();78    public MultiTouchView(Context context) {9        super(context);
10        init();
11    }
12
13    public MultiTouchView(Context context, @Nullable AttributeSet attrs) {
14        super(context, attrs);
15        init();
16    }
17
18    public MultiTouchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
19        super(context, attrs, defStyleAttr);
20        init();
21    }
22
23    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();34    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    }
30
31    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) {23    canvas.drawColor(Color.GREEN);4    if (haveSecondPoint) {5        canvas.drawCircle(point.x, point.y, 50, mDefaultPaint);6    }78    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自定义控件建立完整的认识。

Android开发时的多点触控是如何实现的?相关推荐

  1. Android开发实例之多点触控程序

    智能终端设备的多点触控操作为我们带来了种种炫酷体验,这也使得很多Android开发者都对多点触控程序的开发感兴趣.实际上多点触控程序的实现并不是那么遥不可及,而是比较容易.本文就主要通过一个实例具体讲 ...

  2. Android游戏开发:SurfaceView多点触控之完美钢琴游戏Demo

    一.我们在使用SurfaceView开发小游戏时,如果需要在窗体上自绘按钮和可交互对象,这时需要监听屏幕的多点触控,并且每次触控的改变都需要和游戏产生交互,如何实现呢? CSDN博客 @MXout 有 ...

  3. Android自定义View的多点触控

    在Android游戏开发中,自定义View的多点触控技术必不可少,本文主要简单讲解下Android中多点触控技术的基础知识. 所谓多点触控技术,就是手机屏幕上支持同时处理多个触控点的触屏或移动事件.多 ...

  4. ios触摸超出_iOS开发笔记之多点触控(一)处理触摸的4个方法

    多点触控乃苹果公司带给世界的创新之首,作为移动开发者,熟练掌握多点触控开发技能很有必要. 处理触摸的四个方法: -(void)touchesBegan:(NSSet *)touches withEve ...

  5. Android自定义控件ImageViwe(四)——多点触控实现图片的自由移动

    效果图: 功能 : 可以随手指进行自由移动图片 按照适当的比例设置图片的显示 首先将图片按照适当的比例显示在自定义控件中(当图片的宽度或者高度大于控件的宽度或者高度的时候,会对图片进行适当的缩放,当图 ...

  6. android如何怎么禁止多点触控

    不积跬步无以至千里 在一个页面里做了个对讲的操作,对讲需要长按操作,但是发现碰触界面其他位置,会中断,通过监听这个View的onTouchEvent,其中的MotionEvent中的action,发现 ...

  7. Android:禁用全局多点触控

    在application引用的Theme中添加以下代码: <item name="android:windowEnableSplitTouch">false</i ...

  8. android 多点触控缩放,【移动开发】Android中图片的多点触控和缩放

    前几天做项目用到相机拍照,之后能对图片进行缩放,拖拽,在此我将其单独抽取出来,后面用到时直接拿来用就行了! 效果图: 注:这里不仅能按钮缩放,还能多点触摸缩放和拖拽功能! 1.布局: android: ...

  9. 模拟Android多点触控

    Android多点触控 Android多点触控 多点触控实现思路 第一种adb shell input方式 第二种adb shell sendevent方式 多点触控实现思路   经过资料的查询,要在 ...

  10. Android笔记:触摸事件的分析与总结----多点触控

       其他相关博文:    Android笔记:触摸事件的分析与总结----MotionEvent对象    Android笔记:触摸事件的分析与总结----TouchEvent处理机制     An ...

最新文章

  1. 有没有一种方法可以缓存GitHub凭证来推送提交?
  2. 数据回发时,维护ASP.NET Tree控件位置
  3. Vue3中遇到问题:PostCSS plugin tailwindcss requires PostCSS 8 解决方案
  4. Inno Setup 5制作安装程序
  5. 分享四款非常好用的命令行软件,值得收藏!
  6. Angularjs切换网站配色模式简单示例2(切换body元素的class)
  7. Zdenek Kalal的TLD Tracker(牛啊,学习!)
  8. 软件工程复习提纲——第三章
  9. Arcgis Engine矢量裁剪栅格,调用Mask工具相关代码
  10. spring p2p项目html,springboot2.x项目实战视频教程p2p金融中等项目
  11. 中断 http请求 正在加载 取消http请求
  12. 全国车辆限行限号数据接口服务评测
  13. mysql front的使用注意要点
  14. 轻量级git服务gogs平台
  15. 18位身份证标准及验证
  16. MySQL讲义第50讲——select 查询之查询练习(八):查询每门课程成绩前三名的学生信息
  17. 服务器被攻击 显示503,打开网页后出现503 service unavailable等字样,什么意思
  18. 关于安装Office之后,右键新建菜单中没有Word、PPT、Excel选项
  19. (c语言详解)07-图6 旅游规划(详细解释)
  20. 求助!win10这段时间在使用其间经常性的系统中断这个进程100%cpu的情况

热门文章

  1. SpringBoot参数传递bean自动填充
  2. QString字符串中双引号的梗
  3. 关于Easy ui 操作 控件 disable 整理
  4. 【C#】开发可以可视化操作的windows服务
  5. SVN server
  6. Silverlight开源项目与第三方控件收集
  7. 41.django中auth用户认证
  8. Java多线程 ReentrantLock、Condition 实现生产者、消费者协作模式
  9. Android 滑动定位+吸附悬停效果实现
  10. Maven刷新后jdk变成jre