【朝花夕拾】Android自定义View篇之(八)多点触控(上)MotionEvent简介
前言
在前面的文章中,介绍了不少触摸相关的知识,但都是基于单点触控的,即一次只用一根手指。但是在实际使用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的一些基础知识点,以及引入多点触控。在后面系列文章中,会着重介绍多点触控相关的知识点,以及通过多点触控解决实际工作中的问题。
同样,如果有描述不妥或者不准确的地方,欢迎来拍砖,感谢!
参看文章
【电容式触摸屏】
【朝花夕拾】Android自定义View篇之(八)多点触控(上)MotionEvent简介的更多相关文章
- 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象
前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五 ...
- 【朝花夕拾】Android自定义View篇之(九)多点触控(下)实践出真知
前言 在上一篇文章中,已经总结了MotionEvent以及多点触控相关的基础理论知识和常用的函数.本篇将通过实现单指拖动图片,多指拖动图片的实际案例来进行练习并实现一些效果,来理解前面的理论知识.要理 ...
- 【朝花夕拾】Android自定义View篇之(四)自定义View的三种实现方式及自定义属性使用介绍
前言 转载请声明,转自[https://www.cnblogs.com/andy-songwei/p/10979161.html],谢谢! 尽管Android系统提供了不少控件,但是有很多酷炫效果仍然 ...
- Android自定义View(CustomCalendar-定制日历控件)
转载请标明出处: http://blog.csdn.net/xmxkf/article/details/54020386 本文出自:[openXu的博客] 目录: 1分析 2自定义属性 3onMeas ...
- Android自定义View(三、深入解析控件测量onMeasure)
转载请标明出处: http://blog.csdn.net/xmxkf/article/details/51490283 本文出自:[openXu的博客] 目录: onMeasure什么时候会被调用 ...
- (一)自定义ImageView,初步实现多点触控、自由缩放
真心佩服那些一直专注于技术共享的大神们,正是因为他们无私的分享精神,我才能每天都有进步.近日又算是仔细学了android的自定义控件技术,跟着大神的脚步实现了一个自定义的ImageView.里面涉及到 ...
- 【朝花夕拾】Android自定义View篇之(一)View绘制流程
前言 转载请申明转自[https://www.cnblogs.com/andy-songwei/p/10955062.html]谢谢! 自定义View.多线程.网络,被认为是Android开发者必须牢 ...
- 【朝花夕拾】Android自定义View篇之(五)Android事件分发机制(上)Touch三个重要方法的处理逻辑
前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/10998855.html]谢谢! 在自定义View中,经常需要处理Android事件分发的问题, ...
- 【朝花夕拾】Android自定义View篇之(十一)View的滑动,弹性滑动与自定义PagerView
前言 由于手机屏幕尺寸有限,但是又经常需要在屏幕中显示大量的内容,这就使得必须有部分内容显示,部分内容隐藏.这就需要用一个Android中很重要的概念——滑动.滑动,顾名思义就是view从一个地方移动 ...
随机推荐
- Apache和Tomcat的整合
1.web架构 首先上图,解释web通用架构 通常情况下分为三大块 : ★ Web server : 通常情况下由 Apache Http Server . IBM Http Server .I ...
- C# 有道API翻译 查询单词详细信息
原文:C# 有道API翻译 查询单词详细信息 有道云官方文档 有道云翻译API简介:http://ai.youdao.com/docs/doc-trans-api.s#p01 有道云C#Demo : ...
- Android 5.0(L) ToolBar(替代ActionBar) 现实(四)
经过三天休息,我回来了,我们继续讨论Toolbar. 在此之前假设您正在步步紧跟下来的序列,然后,你应该注意到MainActivity据说他已被警告.因为他们,我们声明toolbar对象.但一直没有用 ...
- JS 扩展方法prototype
通过类对象的prototype设置扩展方法,下面为String对象增加quote(两边加字符)方法 <script type="text/javascript"> St ...
- Qt设置窗口的初始大小(使用sizeHint这个虚函数,或者在构造函数里使用resize函数)
我们用qt创建一个窗口,先后显示它,代码如下: class Mywindow : public QMainWindow{ ..... } int main( int argc, char** argv ...
- win7 64 下安装MyGeneration 遇到的问题解决方法
win7 64 下安装MyGeneration 遇到的问题 ---------------------------MyGeneration 1.3 Setup-------------------- ...
- Win32 Application基本框架
//程序入口 intAPIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdSh ...
- strlen源码剖析(可查看glibc和VC的CRT源代码)
学习高效编程的有效途径之一就是阅读高手写的源代码,CRT(C/C++ Runtime Library)作为底层的函数库,实现必然高效.恰好手中就有glibc和VC的CRT源代码,于是挑了一个相对简单的 ...
- 手机软件没过多久就删了 APP到底得了什么病?
直击现场 PC互联网时代正渐行渐远,移动互联网的创业浪潮汹涌而至.2014年,中国成为拥有智能手机用户最多的国家,而疯狂生长的APP正占据新的风口.据了解,目前我国主要应用商店的APP已累计超过400 ...
- 为什么不用C++写游戏(聪明的程序员不用C++折磨自己)(这些工作,QT都替开发者解决了,C++没有根类导致太多的问题,也没有字符串类)
当今世界上绝大多数游戏都是C++写的,为什么要说不呢? 要做什么?写游戏. 写游戏首先要考虑些什么?做什么样的游戏,图形.音效.游戏逻辑如何实现. 用C++要先考虑什么?定义跨平台数据类型抽象,实现常 ...