前言

在前面的文章中,介绍了不少触摸相关的知识,但都是基于单点触控的,即一次只用一根手指。但是在实际使用App中,常常是多根手指同时操作,这就需要用到多点触控相关的知识了。多点触控是在Android2.0开始引入的,在现在使用的Android手机上都是支持多点触控的。本文将对常见的多点触控相关的重点知识进行总结,并使用多点触控来实现一些常见的效果,从而达到将理论知识付诸实践的目的。

一、触摸事件感应的产生原理

在介绍多点触控前,我们先了解一下现在手机屏幕触摸事件感应的原理。 当前手机使用的屏幕一般都是电容式触摸屏,我们看看百度百科中对此的介绍:

电容式触摸屏技术是利用人体的电流感应进行工作的。当手指触摸在屏幕上时,由于人体电场,用户和触摸屏表面形成以一个耦合电容,对于高频电流来说,电容是直接导体,于是手指从接触点吸走一个很小的电流。这个电流分别从触摸屏的四角上的电极中流出,并且流经这四个电极的电流与手指到四角的距离成正比,控制器通过对这四个电流比例的精确计算,得出触摸点的位置。 (摘自百度百科【电容式触摸屏】)

电容式触摸屏感应触摸事件,和人体电场相关,这也就是为什么用手指触摸时屏幕能有响应,但其它物体却不行的原因。而早期的手机采用的是电阻式触摸屏,当屏幕受到压力时电阻有变化,通过电阻来感应触摸,所以除了手指外,其它物体也能让屏幕产生响应。电容式触摸屏支持多点触控,但电阻式触摸屏不能。

二、触摸事件与底层

在文章【【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象】的开头我们介绍过“事件的前世今生”,事件是从硬件感应,然后经过驱动、框架,然后到达View的。前面讲过的内容这里不再赘述,我们看看下面这份截图:

这是MotionEvent类中跟踪与事件相关的主要方法的结果,几乎都是很快就调到了native层。通过这些方法,我们可以直观感受到事件与底层的密切联系。

三、事件输入设备以及MotionEvent中对应的事件说明

随着Android系统版本的提升,以及Android硬件设备的发展,事件输入设备和对应的事件特点也在不断发生着变化。轨迹球出现在很早的手机中,后来去掉了;多点触控也是在Android2.0开始支持的......咱们这里不一一列举,当然,大家也不关心这些细节。这里我汇总了目前我知道的一些事件输入设备,以及在MotionEvent中封装的对应的响应事件。

如下表格显示了它们大概的对应关系,由于我使用过的设备有限,所以有些对应设备的对应关系不太确定,下表中在括号内加了“?”。注意我这里的措词是“大概”,因为下面有些对应关系可能有交叉的情况等。本文关注的重点是多点触控,其它的这里咱们只做了解即可。

输入设备 响应事件 事件常量值 事件说明

单点触控/
触控笔/
多点触控/
橡皮檫(?)

ACTION_DOWN 0 第一个手指初次接触到屏幕时触发。
ACTION_UP 1 手指在屏幕上滑动时触发,会多次触发。
ACTION_MOVE 2 最后一个手指离开屏幕时触发。
ACTION_CANCEL 3 当前的手势被中断时触发。
ACTION_OUTSIDE 4 事件发生在UI边界之外时触发。
ACTION_POINTER_DOWN 5 有非主要的手指按下(即按下之前已经有手指在屏幕上)。
ACTION_POINTER_UP 6 有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。
鼠标/轨迹球(?) ACTION_HOVER_MOVE 7 指针在窗口或者View区域移动,但没有按下。
ACTION_SCROLL 8 滚轮滚动,可以触发水平滚动或垂直滚动
ACTION_HOVER_ENTER 9 指针移入到窗口或者View区域,但没有按下。
ACTION_HOVER_EXIT 10 指针移出到窗口或者View区域,但没有按下。

键盘/操纵杆(?)/
遥控器/
游戏控制器(游戏手柄)

ACTION_BUTTON_PRESS 11 按钮被按下
ACTION_BUTTON_RELEASE 12 按钮被释放
多点触控 ACTION_POINTER_1_DOWN 0x0005 第 2 个手指按下,android2.2后已废弃,不推荐使用。
ACTION_POINTER_2_DOWN 0x0105 第 3 个手指按下,android2.2后已废弃,不推荐使用。
ACTION_POINTER_3_DOWN 0x0205 第 4 个手指按下,android2.2后已废弃,不推荐使用。
ACTION_POINTER_1_UP 0x0006 第 2 个手指抬起,android2.2后已废弃,不推荐使用。
ACTION_POINTER_2_UP 0x0106 第 3 个手指抬起,android2.2后已废弃,不推荐使用。
ACTION_POINTER_3_UP 0x0206 第 4 个手指抬起,android2.2后已废弃,不推荐使用。

四、触摸事件与多点触控

前面我们在处理单点触控问题的时候,是在onTouchEvent(MotionEvent event)方法中通过使用event.getAction()来获取事件常量进行判断的。在Android2.0开始,要获取多点触控的事件,需要使用event.getActionMask()。如下所示:

 @RequiresApi(api = Build.VERSION_CODES.KITKAT)
 @Override
 public boolean onTouchEvent(MotionEvent event) {
     Log.i(TAG, "event=" + MotionEvent.actionToString(event.getActionMasked()));
     switch (event.getActionMasked()) {
         ......
     }
     return super.onTouchEvent(event);
 }

这里MotionEvent.actionToString(int)是系统提供的方法,可以将int表示的事件转为字符串,方便观察。方法的源码,读者可以自己去看看,很简单。

实际上在现在的系统版本中event.getAction()仍然能获取多指事件,这些获取的事件在上述表格中有说明,即上表中ACTION_POINTER_1_DOWN到ACTION_POINTER_3_UP,如果手指更多,事件也会更多。但是这个用法在Android2.0开始就被废弃了,现在需要兼容到2.0以下的场景太少了,所以这些过时的做法就不再介绍了,只要知道有这么回事就可以了。

这一节介绍使用event.getActionMask()方法后获取的几个触摸相关的事件。ACTION_DOWN和ACTION_UP前面的文章已经介绍过多次了,前的表格中也有说明,这里就不赘述了。

1、ACTION_CANCEL

这个事件在整个事件流被中断时会调用,比如父布局把ACTION_DOWN事件分发给了子View,但后面的MOVE和UP事件却给拦截时,子View中会产生CANCEL事件。ACTION_CANCEL事件和ACTION_UP事件总有一个会产生,实际上不少场景下会把ACTION_CANCEL当做ACTION_UP对待,来处理当前的事件流。在前面的文章【【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象】的第四节介绍requestDisallowInterceptTouchEvent(true)的作用时,就演示过ACTION_CANCEL的产生,这里不赘述了,不明白的可以去这篇文章看看。

还有一种常见的情形,ListView的使用场景。当手指触摸ListView时,会把ACTION_DOWN事件分发给ItemView,但是当手指开始滑动时,ListView发现这个时候需要自己消费这个滑动事件了,于是就把后续的MOVE和UP事件给拦截掉。ItemView被调侃了,绝望之下只能调用ACTION_CANCEL事件了。

这个事件算是一种比较特殊的事件了。

2、ACTION_OUTSIDE

这个事件比ACTION_CANCEL更特殊,一般很难触发。官方的介绍说是事件发生UI控件边界之外时触发,但通过实验,死活都触发不了这个事件。事实上这个事件出现的场景比较少见,我目前知道PopWindow和Dialog使用时可能触发这个场景。这里简单介绍一下使用Dialog时触发该事件的场景。

先自定义一个如下的Dialog:

 public class CustomDialog extends Dialog {
     public CustomDialog(Context context) {
         super(context);
         init();
     }

     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         if (MotionEvent.ACTION_OUTSIDE == event.getAction()) {
             Log.i("songzheweiwang", MotionEvent.actionToString(event.getAction()));
         }
         return super.onTouchEvent(event);
     }

     private void init() {
         setContentView(R.layout.dialog_outside);
         //清空原有的flag
         getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
         //设置监听OutSide Touch
         getWindow().setFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
     }
 }

注意第19行和第21行,需要设置相应的flag。

点击界面的对话框以外的区域,可以看到如下log(对话框的显示和布局比较简单,这里就不贴出来了):

07-04 07:22:57.719 15647-15647/com.example.demos I/songzheweiwang: ACTION_OUTSIDE

3、ACTION_POINTER_DOWN

第二根手指以及更多的手指触摸时都会触发这个事件,不能从这个事件中判断是第几根手指。每根手指的事件都封装在MotionEvent中了,要想判断是第几根手指,需要结合MotionEvent提供的getActionIndex(),getPointerId(int),findPointerIndex(int)等方法来确定,具体的使用方法后面会做详细介绍。

4、ACTION_MOVE

无论是哪根手指移动,都会触发该事件。

5、ACTION_POINTER_UP

只要抬起的手指不是最后一根,就会触发这个事件,同样无法直接判断是第几根手指抬起来的。

五、获取事件的位置

在处理多点触控的时候,往往需要获取事件发生点的位置信息来完成一些效果。MotionEvent提供了多个用于获取事件位置的方法,一般处理事件是在View中来完成的,View本身也提供了一些判断自身位置的方法,并且这些方法名称和功能都非常相似,这导致在实际开发中,很容易混淆。这里我们简单了解并辨别这些方法的功能,如下表所示:

 
研究对象 方法名称 方法作用说明
View getLeft() 获取该View左边界与直接父布局左边界的距离。以直接父布局左上顶点为原点的坐标系为参照。
getTop() 获取该View上边界与直接父布局上边界的距离。
getX() 获取该View左上顶点在坐标系上的X坐标值。参照的坐标系同上。
getY() 获取该View左上顶点在坐标系上的Y坐标值。
MotionEvent getX() 获取事件相对于所在View的X坐标值。即以所在View的左上顶点为原点的坐标系为参照。
getY() 获取事件相对于所在View的Y坐标值。
getX(int pointerIndex) 获取给定pointerIndex的事件的X坐标值。该值也是相对于所在View而言的。
getY(int pointerIndex) 获取给定pointerIndex的事件的Y坐标值。
getRawX() 获取事件与屏幕左边界的距离。即以屏幕左上角为原点的坐标系为参照。
getRawY() 获取事件与屏幕顶部边界的距离。

通过上表,我们发现,最重要的是要搞清楚各个方法所参照的坐标系。为了直观了解各个方法获取的值的含义,我们参照上面的表格和下图进行理解。

这其中涉及到的三个坐标系分别为:

  • View的getX()/getY()/getLeft()/getTop()所参照的,都是以直接父控件的左上角顶点为原点的坐标系,即图中标注的坐标系。这里getX()和getLeft(),getY()和getTop()的返回值是一样的。
  • MotionEvent的getX()/getY()/getX(int pointerIndx)/getY(int pointerIndex)所参照的,是以当前所在的View的左上角顶点为原点的坐标系。后面两个方法,是用于多点触控中获取对应事件的坐标位置的,后面会再讲到。
  • getRawX()/getRawY()所参照的,是以整个屏幕左上角顶点为原点的坐标系。getRawY()的值是包含了标题栏和状态栏高度的。

咱们用数据说话,这里看看演示结果。自定义一个view,在onTouchEvent方法中打印出上述各个方法获取的值。

 public class CustomView extends View {
     private static final String TAG = "CustomView";

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

     @Override
     public boolean onTouchEvent(MotionEvent event) {
         float viewLeft = getLeft();
         float viewTop = getTop();
         float viewX = getX();
         float viewY = getY();
         float eventX = event.getX();
         float eventY = event.getY();
         float rawX = event.getRawX();
         float rawY = event.getRawY();
         int index = event.getActionIndex();
         float pointerX = event.getX(index);
         float pointerY = event.getY(index);
         Log.i(TAG, "viewLeft=" + viewLeft + ";viewTop=" + viewTop
                 + ";\n viewX=" + viewX + ";viewY=" + viewY
                 + ";\n eventX=" + eventX + ";eventY=" + eventY
                 + ";\n rawX=" + rawX + ";rawY=" + rawY
                 + ";\n index=" + index + ";pointerX=" + pointerX + ";pointerY=" + pointerY);
         return super.onTouchEvent(event);
     }
 }

布局效果如前面的截图所示,

 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">

     <com.example.demos.customviewdemo.CustomView
         android:layout_width="200dp"
         android:layout_height="200dp"
         android:layout_centerHorizontal="true"
         android:layout_marginTop="100dp"
         android:background="@android:color/darker_gray" />
 </RelativeLayout>

触摸界面中的自定义View,抓取ACTION_DOWN事件的log如下所示:

viewLeft=240.0;viewTop=300.0;
viewX=240.0;viewY=300.0;
eventX=387.0;eventY=424.0;
rawX=627.0;rawY=1003.0;
index=0;pointerX=387.0;pointerY=424.0

当前的测试机density=3.0,且标题栏和状态栏的高度值之和为279px。通过打印结果中正好rawY = eventY + viewY + 279,和前面给的结论对应上了。

这里需要注意的是getX()和getY()这个方法,在单点触摸的时候很好理解,因为同时只有一个事件,但在多点触摸中,就不太好理解了。如下是两个手指触摸捕捉到的log:

ACTION_DOWN
viewLeft=240.0;viewTop=300.0;viewX=240.0;viewY=300.0;eventX=380.0;eventY=215.0;rawX=620.0;rawY=794.0;index=0;pointerX=380.0;pointerY=215.0
ACTION_POINTER_DOWN(0)
viewLeft=240.0;viewTop=300.0;viewX=240.0;viewY=300.0;eventX=380.0;eventY=215.0;rawX=620.0;rawY=794.0;index=1;pointerX=206.0;pointerY=364.0
ACTION_POINTER_UP(0)
viewLeft=240.0;viewTop=300.0;viewX=240.0;viewY=300.0;eventX=380.0;eventY=215.0;rawX=620.0;rawY=794.0;index=0;pointerX=380.0;pointerY=215.0
ACTION_UP
viewLeft=240.0;viewTop=300.0;viewX=240.0;viewY=300.0;eventX=206.0;eventY=364.0;rawX=446.0;rawY=943.0;index=0;pointerX=206.0;pointerY=364.0

前三个事件时,eventX和eventY的值是一样的。ACTION_POINTER_DOWN(0)表示有第二根手指按下了,ACTION_POINTER_UP(0)表示其中一根手指抬起来了。按照我们的理解,另外一个手指按下了,eventX和eventY应该记录的是第二根手指按下的事件的坐标才对,不可能和第一根手指按下的事件坐标一样。所以这里就是需要着重注意的地方,我们先看看官网API中对它的描述:

public float getX ()
getX(int) for the first pointer index (may be an arbitrary pointer identifier).

描述中说,该方法获取的是第一个pointerIndex对应事件的坐标,即pointerIndex = 0对应的手指的触摸事件坐标(这里我是根据实验的结果和官网的说明来下的结论,不保证完全正确,请注意)。括号中也补充说明了,也有可能是一个随意的Pointer标识符。看到这里,我们应该可以明白上述log中的现象了吧。

结语

由于MotionEvent和多点触控相关的知识点比较多,所以一篇文章很难讲主要知识点介绍完。本文主要介绍了MotionEvent的一些基础知识点,以及引入多点触控。在后面系列文章中,会着重介绍多点触控相关的知识点,以及通过多点触控解决实际工作中的问题。

同样,如果有描述不妥或者不准确的地方,欢迎来拍砖,感谢!

参看文章

安卓自定义View进阶-MotionEvent详解

电容式触摸屏

【朝花夕拾】Android自定义View篇之(八)多点触控(上)MotionEvent简介的更多相关文章

  1. 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象

    前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五 ...

  2. 【朝花夕拾】Android自定义View篇之(九)多点触控(下)实践出真知

    前言 在上一篇文章中,已经总结了MotionEvent以及多点触控相关的基础理论知识和常用的函数.本篇将通过实现单指拖动图片,多指拖动图片的实际案例来进行练习并实现一些效果,来理解前面的理论知识.要理 ...

  3. 【朝花夕拾】Android自定义View篇之(四)自定义View的三种实现方式及自定义属性使用介绍

    前言 转载请声明,转自[https://www.cnblogs.com/andy-songwei/p/10979161.html],谢谢! 尽管Android系统提供了不少控件,但是有很多酷炫效果仍然 ...

  4. Android自定义View(CustomCalendar-定制日历控件)

    转载请标明出处: http://blog.csdn.net/xmxkf/article/details/54020386 本文出自:[openXu的博客] 目录: 1分析 2自定义属性 3onMeas ...

  5. Android自定义View(三、深入解析控件测量onMeasure)

    转载请标明出处: http://blog.csdn.net/xmxkf/article/details/51490283 本文出自:[openXu的博客] 目录: onMeasure什么时候会被调用 ...

  6. (一)自定义ImageView,初步实现多点触控、自由缩放

    真心佩服那些一直专注于技术共享的大神们,正是因为他们无私的分享精神,我才能每天都有进步.近日又算是仔细学了android的自定义控件技术,跟着大神的脚步实现了一个自定义的ImageView.里面涉及到 ...

  7. 【朝花夕拾】Android自定义View篇之(一)View绘制流程

    前言 转载请申明转自[https://www.cnblogs.com/andy-songwei/p/10955062.html]谢谢! 自定义View.多线程.网络,被认为是Android开发者必须牢 ...

  8. 【朝花夕拾】Android自定义View篇之(五)Android事件分发机制(上)Touch三个重要方法的处理逻辑

    前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/10998855.html]谢谢! 在自定义View中,经常需要处理Android事件分发的问题, ...

  9. 【朝花夕拾】Android自定义View篇之(十一)View的滑动,弹性滑动与自定义PagerView

    前言 由于手机屏幕尺寸有限,但是又经常需要在屏幕中显示大量的内容,这就使得必须有部分内容显示,部分内容隐藏.这就需要用一个Android中很重要的概念——滑动.滑动,顾名思义就是view从一个地方移动 ...

随机推荐

  1. WPF无边框捕获消息改变窗口大小

    原文:WPF无边框捕获消息改变窗口大小 文章大部分转载自http://blog.csdn.net/fwj380891124,如有问题,请联系删除  最近一直在学习 WPF,看着别人做的WPF程序那么漂 ...

  2. JDBC学习笔记——增删改查

    1.数据库准备  要用JDBC操作数据库,第一步当然是建立数据表: ? 1 2 3 4 5 6 CREATE TABLE `user` (   `id` int(11) NOT NULL AUTO_I ...

  3. Lua学习 2) —— Android与Lua互调

    2014-07-09 一.Android类调用lua并回调 Android调用Lua函数,同一时候把类作为參数传递过去.然后再Lua中回调类的函数 调用lua mLuaState = LuaState ...

  4. WPF教程002 - 实现Step步骤条控件

    原文:WPF教程002 - 实现Step步骤条控件 在网上看到这么一个效果,刚好在用WPF做控件,就想着用WPF来实现一下 1.实现原理 1.1.该控件分为2个模块,类似ComboBox控件分为Ste ...

  5. maven私服nexus安装

    maven私服nexus安装 1.nexus特性 1.1.nexus私服实际上是一个javaEE的web 系统 1.2.作用:用来管理一个公司所有的jar包,实现项目jar包的版本统一 1.3.jar ...

  6. C++ CGI开发环境备录

    1. 安装apache2: apt-get install apache2 2. 配置用户目录 在/etc/apache2/apache2.conf中配置用户目录 <Directory /hom ...

  7. ios 调用系统发短信以及打电话功能

    先介绍一种最简单的方法: 调用打电话功能 [[UIApplicationsharedApplication] openURL:[NSURL URLWithString:@"tel://100 ...

  8. 零元学Expression Blend 4 - Chapter 28 ListBox的基本运用与更改预设样式

    原文:零元学Expression Blend 4 - Chapter 28 ListBox的基本运用与更改预设样式 本章将先教大家认识ListBox的基本运用与更改预设样式 本章将先教大家认识List ...

  9. 用C#修改系统区域和语言设置

    原文:用C#修改系统区域和语言设置 这几天做项目,因为客户机的系统不同,发现客户机的区域和语言设置也不尽相同,导致程序运行时根据时间判断的很多属性和方法都出现各种各样的千奇百怪的问题. 修改程序太费时 ...

  10. Java HashMap实现原理 源码剖析

    HashMap是基于哈希表的Map接口实现,提供了所有可选的映射操作,并允许使用null值和null建,不同步且不保证映射顺序.下面记录一下研究HashMap实现原理. HashMap内部存储 在Ha ...