【朝花夕拾】Android自定义View篇之(八)多点触控(上)基础知识总结
前言
转载请声明,转自【https://www.cnblogs.com/andy-songwei/p/11155259.html】,谢谢!
在前面的文章中,介绍了不少触摸相关的知识,但都是基于单点触控的,即一次只用一根手指。但是在实际使用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 | 多指按下时,第一个手指抬起,然后再按下时会触发 |
ACTION_POINTER_2_DOWN | 0x0105 | 第 2 个手指按下,android2.2后已废弃,不推荐使用。 | |
ACTION_POINTER_3_DOWN | 0x0205 | 第 3 个手指按下,android2.2后已废弃,不推荐使用。 | |
ACTION_POINTER_1_UP | 0x0006 | index=0,但不是最后一个手指抬起时触发 | |
ACTION_POINTER_2_UP | 0x0106 | 第 2 个手指抬起,android2.2后已废弃,不推荐使用。 | |
ACTION_POINTER_3_UP | 0x0206 | 第 3 个手指抬起,android2.2后已废弃,不推荐使用。 |
特别注意:表格中“ACTION_POINTER_1_DOWN”和“ACTION_POINTER_1_UP”两个常量,我看到过有一些知名博客中对它们的描述是:第二根手指按下/抬起,已废弃,不推荐使用。我通过实验发现这个说法是错误的,所以特地纠正一下。如下是验证的代码和打印的结果:
1 @Override 2 public boolean onTouchEvent(MotionEvent event) { 3 Log.i(TAG, MotionEvent.actionToString(event.getAction()) + ";action=" + event.getAction()); 4 return super.onTouchEvent(event); 5 }
依次按下和抬起两根手指,打印结果如下:
07-05 22:24:47.982 23249-23249/com.example.demos I/songzheweiwang: ACTION_DOWN;action=0
07-05 22:24:48.511 23249-23249/com.example.demos I/songzheweiwang: ACTION_POINTER_DOWN(1);action=261
07-05 22:24:49.599 23249-23249/com.example.demos I/songzheweiwang: ACTION_POINTER_UP(1);action=262
07-05 22:24:49.607 23249-23249/com.example.demos I/songzheweiwang: ACTION_UP;action=1
可以看到,整个过程中就没有打印“ACTION_POINTER_1_DOWN”和“ACTION_POINTER_1_UP”这两个值,而是分别对应打印的“ACTION_POINTER_2_DOWN”和“ACTION_POINTER_2_UP”。
在前面的表格中可以看到“ACTION_POINTER_1_DOWN”和“ACTION_POINTER_1_UP”这两个值对应的十进制值分别和“ACTION_POINTER_DOWN”和“ACTION_POINTER_UP”相等,这两个值只有在Android2.2支持多点触控后,系统提供的getActionMasked()方法中才会用到。实际上,通过实验发现,当多指按下后,第一个按下的手指在抬起后,再按下时会触发ACTION_POINTER_1_DOWN,即按下时,pointerIndex为0的手指会触发这个事件(pointerIndex后面会再介绍)。同样,在pointerIndex为0,但它又不是最后一根手指抬起时,会触发ACTION_POINTER_1_UP事件。虽然这是过时的事件,但对理解多点触控还是有很大帮助的。
另外,官网上给的常量值是按照32位来表示的,源码上用的是16位来表示的,不过这并没有什么影响,我这里按照源码中的来讲。
再牛X的博主也有出错的时候,不要太迷信权威,有歧义的时候最好还是通过实验来验证一下比较好。
四、触摸事件与多点触控
前面我们在处理单点触控问题的时候,是在onTouchEvent(MotionEvent event)方法中通过使用event.getAction()来获取事件常量进行判断的。在Android2.0开始,要获取多点触控的事件,需要使用event.getActionMask()。如下所示:
1 @RequiresApi(api = Build.VERSION_CODES.KITKAT) 2 @Override 3 public boolean onTouchEvent(MotionEvent event) { 4 Log.i(TAG, "event=" + MotionEvent.actionToString(event.getActionMasked())); 5 switch (event.getActionMasked()) { 6 ...... 7 } 8 return super.onTouchEvent(event); 9 }
这里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:
1 public class CustomDialog extends Dialog { 2 public CustomDialog(Context context) { 3 super(context); 4 init(); 5 } 6 7 @RequiresApi(api = Build.VERSION_CODES.KITKAT) 8 @Override 9 public boolean onTouchEvent(MotionEvent event) { 10 if (MotionEvent.ACTION_OUTSIDE == event.getAction()) { 11 Log.i("songzheweiwang", MotionEvent.actionToString(event.getAction())); 12 } 13 return super.onTouchEvent(event); 14 } 15 16 private void init() { 17 setContentView(R.layout.dialog_outside); 18 //清空原有的flag 19 getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL); 20 //设置监听OutSide Touch 21 getWindow().setFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); 22 } 23 }
注意第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方法中打印出上述各个方法获取的值。
1 public class CustomView extends View { 2 private static final String TAG = "CustomView"; 3 4 public CustomView(Context context, @Nullable AttributeSet attrs) { 5 super(context, attrs); 6 } 7 8 @Override 9 public boolean onTouchEvent(MotionEvent event) { 10 float viewLeft = getLeft(); 11 float viewTop = getTop(); 12 float viewX = getX(); 13 float viewY = getY(); 14 float eventX = event.getX(); 15 float eventY = event.getY(); 16 float rawX = event.getRawX(); 17 float rawY = event.getRawY(); 18 int index = event.getActionIndex(); 19 float pointerX = event.getX(index); 20 float pointerY = event.getY(index); 21 Log.i(TAG, "viewLeft=" + viewLeft + ";viewTop=" + viewTop 22 + ";\n viewX=" + viewX + ";viewY=" + viewY 23 + ";\n eventX=" + eventX + ";eventY=" + eventY 24 + ";\n rawX=" + rawX + ";rawY=" + rawY 25 + ";\n index=" + index + ";pointerX=" + pointerX + ";pointerY=" + pointerY); 26 return super.onTouchEvent(event); 27 } 28 }
布局效果如前面的截图所示,
1 <?xml version="1.0" encoding="utf-8"?> 2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent"> 5 6 <com.example.demos.customviewdemo.CustomView 7 android:layout_width="200dp" 8 android:layout_height="200dp" 9 android:layout_centerHorizontal="true" 10 android:layout_marginTop="100dp" 11 android:background="@android:color/darker_gray" /> 12 </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中的现象了吧。
六、多点触控重难点
在多点触控中,最难理解的地方应该是pointerIndex和pointerId的理解和使用了,当然这不仅是难点,也是重点,应该在处理很多多点触控的问题时,都需要涉及到它们。
1、主要手指和非主要手指
在分析多点触控时,我们需要先理解两个概念:主要手指和主要手指。在手指按下时,主要手指是指第一个按下的手指,其它后面按下的手指就是非主要手指。在手指抬起时,主要手指是指最后一个离开屏幕的手指,提前离开的为非主要手指。所以整个过程中,主要手指和非主要手指是会变化的,因为第一个按下的手指很有可能不是最后一个离开屏幕的,“皇帝轮流做,今天到我家”嘛,这一点需要理解清楚!所以ACTION_DOWN和ACTION_UP都是主要手指产生的事件,ACTION_POINTER_DOWN和ACTION_POINTER_UP是非主要手指事件。
2、手指的编码pointerId
在前面说过,在多点触控中,除第一根手指外,其他手指按下时,通过getActionMasked()获得的事件都是ACTION_POINTER_DOWN。那么,当多个手指同时按在屏幕上,产生的那么多事件,如何来确定是第几根手指的事件呢?
系统的解决办法是:当每一根手指按下时,为其编号!当手指第一次按下时,系统会为这根手指生成一个唯一的编号,我们这里称之为pointerId。当这个手指抬起时,或者该事件被拦截了,系统会回收这个编号。当需要查看某个手指事件相关信息时,需要通过这个pointerId来找到这个手指。另外,当有手指再次按下时,之前被系统回收的编号可能会再次被使用。
这里我们需要记住一个结论:只要某根手指没有离开屏幕,那么无论中间有多少手指按下抬起,这个手指的pointerId都不会变化(事件被拦截除外)。
3、手指的序号pointerIndex
我们知道了pointerId就像这个手指的身份证一样重要,但是我们怎样才能获取到这个编号呢?很遗憾,系统并没有提供直接得到这个编号的方法,只有在MotionEvent中提供了一个间接的方式:getPointerId(int pointerIndex)。
现在是不是又有疑问了,这个pointerIndex是什么?如何获取?它是做什么用的?
MotionEvent提供了一个方法,getActionIndex(),通过这个方法可获取这个pointerIndex的值。继续看看源码:
1 /** 2 * For {@link #ACTION_POINTER_DOWN} or {@link #ACTION_POINTER_UP} 3 * as returned by {@link #getActionMasked}, this returns the associated 4 * pointer index. 5 * The index may be used with {@link #getPointerId(int)}, 6 * {@link #getX(int)}, {@link #getY(int)}, {@link #getPressure(int)}, 7 * and {@link #getSize(int)} to get information about the pointer that has 8 * gone down or up. 9 * @return The index associated with the action. 10 */ 11 public final int getActionIndex() { 12 return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK) 13 >> ACTION_POINTER_INDEX_SHIFT; 14 }
通过这段源码,我们应该够窥察到pointerIndex的一些用武之处了吧。再继续看看方法体中这些方法的信息:
1 //=============MotionEvent.java=============== 2 ...... 3 public static final int ACTION_POINTER_INDEX_MASK = 0xff00; 4 public static final int ACTION_POINTER_INDEX_SHIFT = 8; 5 private static native int nativeGetAction(long nativePtr); 6 /** 7 *...... 8 * Consider using {@link #getActionMasked} 9 *...... 9 */ 10 public final int getAction() { 11 return nativeGetAction(mNativePtr); 12 } 13 ......
看到这里就明白了,pointerIndex实际上就是getAction()获取的事件值取高8位得到的。getAction()的注释中也说得很明白,建议使用getActionMasked()方法来获取事件,继续看看它的源码:
1 //===========MotionEvent.java========== 2 ...... 3 public static final int ACTION_MASK = 0xff; 4 /** 5 * Return the masked action being performed, without pointer index information. 6 * Use {@link #getActionIndex} to return the index associated with pointer actions. 7 * @return The action, such as {@link #ACTION_DOWN} or {@link #ACTION_POINTER_DOWN}. 8 */ 9 public final int getActionMasked() { 10 return nativeGetAction(mNativePtr) & ACTION_MASK; 11 } 12 ......
我们又发现,系统建议使用的getActionMasked()方法,得到的事件,实际上是getAction()得到的值的低8位表示的。
现在我们明白了,getActionMasked()和getActionIndex()的值分别就是getAction()的低8位和高8位两个部分。这种用一个int来存储两个信息的做法,在Android源码中比较常见,因为pointerIndex和action的范围都很少,单独给每一个分配一个空间,比较浪费。在前面的文章【【朝花夕拾】Android自定义View篇之(一)View绘制流程】中,MeasureSpec就是将Mode和Size整合在一起的例子。到这里,我们就清楚了pointerIndex的来历了。
结合ACTION_POINTER_X_DOWN/UP的值以及对应事件的说明,就能清楚pointerIndex表示的是按下/抬起事件对应手指的序号(正好对应上了这个X值)。那么既然有了pointerIndex了,为啥还要多此一举再搞一个pointerId呢?我总结了一下,大概有两点原因:
(1)现在假设一种场景,食指和中指依次按下,那么通过前面pointerIndex的计算方法,它们的pointerIndex的值分别就是0和1了;在抬起的时候如果也是食指先抬起中指后抬起,那么食指触发的事件为ACTION_POINTER_UP,中指触发的事件为ACTION_UP了,此时食指和中指对应的index就分别变成了1和0了。同一根手指在这个过程中的pointerIndex值变了,可见这个值是动态变化的,我们前面给过一个结论,同一根手指在按下到抬起整个过程中pointerId值是不会变化的,pointerId更稳定。
(2)我们前面也说过,任何一根手指在移动的时候,响应的事件都是ACTION_MOVE,而ACTION_MOVE = 2,经过getActionIndex()计算,得到的pointerIndex值为0,根本无法区分哪根手指,可见在ACTION_MOVE事件中这个值是失效的。而我们知道,在很多场景下我们需要在ACTION_MOVE事件中做事情,关键时刻pointerIndex却掉链子了。在getActionIndex()的源码注释中也做了说明,它用于ACTION_POINTER_DOWN和ACTION_POINTER_UP事件。此时就需要用pointerId来追踪事件流了。
我们可以这样理解,pointerId是触摸手指的身份证,而pointerIndex是住址,住址可能经常变动,在四处奔波中可能连有效住址都没有,但身份证就是跟随一辈子不变化的,这样是不是好记忆多了。这里再简单总结一下它的特点:1)pointerIndex是不固定的;2)pointerIndex对多点触控的down和up事件有效,对move事件无效。
4、pointerId的复用和pointerIndex变化举例
这里,我们通过A,B,C三根手指的按下和抬起,来观察这两个值的变化情况:
事件 | 手指数量 | pointerIndex及pointerId变化 |
A手指按下 | 1 | A手指pointerIndex=0,pointerId=0 |
B手指按下 | 2 | A手指pointerIndex=0,pointerId=0;B手指pointerIndex=1,pointerId=1 |
A手指抬起 | 1 | B手指pointerIndex=0,pointerId=1 |
C手指按下 | 2 | C手指pointerIndex=0,pointerId=0;B手指pointerIndex=1,pointerId=1 |
当A手指抬起后,B手指的pointerIndex从1变成了0;当C手指按下后,B手指的pointerIndex又从0变成了1;B手指的pointerId一直是1,没有变化。C手指按下,C复用了A手指被系统回收的pointerId,值为0。现在应该能够有个直观的感受了吧。而且我们还能得到几个变化规律:
1)按下手指时,从0开始自动增长。
2)如果之前按下的手指抬起,后面的手指会随之减小。
3)无论手指如何变化,当前还在屏幕上的手指的pointerIndex,都是从0开始的连续序列值。
4)刚按下的手指,如果前面的pointerId序列中有空缺,会按照该值的大小由小到大填补前面的空缺,且该手指初始时pointerIndex和pointerId值相等。如果前面pointerId没有空缺,则往后面添加。
5)当有手指抬起,后来又有手指按下,之前留下的手指的pointerIndex变化会趋向于自己第一次按下时的数值,也就是趋向于自己的pointerId值变化。
还有更多的规律,读者可以自己总结。最后再看一组图示来理解一下这个变化过程:
5、多点触控常见的几个方法
除了前提到的getActionMasked()和getActionId()外,MotionEvent类还提供了如下几个常用的方法,用于处理多点触控和获取不同手指的信息。
(1)getPointerCounter()
作用:获取在屏幕上手指的个数
1 /** 2 * The number of pointers of data contained in this event. Always 3 * >= 1. 4 */ 5 public final int getPointerCount() { 6 return nativeGetPointerCount(mNativePtr); 7 } 8 ...... 9 private static native int nativeGetPointerCount(long nativePtr);
(2)getPointerId(int pointerIndex)
作用:获取手指的唯一标识符ID
1 public final int getPointerId(int pointerIndex) { 2 return nativeGetPointerId(mNativePtr, pointerIndex); 3 } 4 .... 5 private static native int nativeGetPointerId(long nativePtr, int pointerIndex);
(3)findPointerIndex(int pointerId)
作用:通过pointerId获取pointerIndex,然后根据pointerIndex来获取该手指事件的相关信息
1 public final int findPointerIndex(int pointerId) { 2 return nativeFindPointerIndex(mNativePtr, pointerId); 3 } 4 ...... 5 private static native int nativeFindPointerIndex(long nativePtr, int pointerId);
(4)getX(int pointerIndex)
作用:获取给定pointerIndex对应手指的X坐标。
1 public final float getX(int pointerIndex) { 2 return nativeGetAxisValue(mNativePtr, AXIS_X, pointerIndex, HISTORY_CURRENT); 3 } 4 ...... 5 private static native float nativeGetAxisValue(long nativePtr, 6 int axis, int pointerIndex, int historyPos);
(5)getY(int pointerIndex)
作用:获取给定pointerIndex对应手指的Y坐标。
1 public final float getY(int pointerIndex) { 2 return nativeGetAxisValue(mNativePtr, AXIS_Y, pointerIndex, HISTORY_CURRENT); 3 } 4 ...... 5 private static native float nativeGetAxisValue(long nativePtr, 6 int axis, int pointerIndex, int historyPos);
从如上的方法可以看出,在获取指定手指的事件信息时,都是通过参数pointerIndex来确定的。我们前面说过pointerIndex就像是家庭住址,pointerId就像身份证号,要找到某个人需要通过他的家庭住址来找,而不是身份证号,这样就容易理解了。另外,这几个方法都是直接调用了native方法,可见触摸事件和底层的依赖程度。
当然,MotionEvent类还提供了很多用于获取历史事件,事件时间,压力大小等的方法,读者可以通过下面的参考文章中了解详细的使用和功能。
参看文章
【电容式触摸屏】
本部分主要介绍基础和理论部分知识,接下来会通过练习和demo来加强理解。同样,如果本文有描述不妥或者不准确的地方,欢迎来拍砖,感谢!