朝花夕拾Android自定义View篇之多点触控(上)基础知识

Posted andy-songwei

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了朝花夕拾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 ---
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()方法中才会用到,用于表示所有非第一个手指的按下和抬起事件。至于这两个值到底做什么用的,我也不敢随意说,官方没有具体说明,再者它们都是过时的常量,咱们就不细究了。

       另外,官网上给的常量值是按照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类还提供了很多用于获取历史事件,事件时间,压力大小等的方法,读者可以通过下面的参考文章中了解详细的使用和功能。

 

参看文章

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

       【安卓自定义View进阶-多点触控详解

       【电容式触摸屏

       【MotionEvent

 

       本部分主要介绍基础和理论部分知识,接下来会通过练习和demo来加强理解。同样,如果本文有描述不妥或者不准确的地方,欢迎来拍砖,感谢!

以上是关于朝花夕拾Android自定义View篇之多点触控(上)基础知识的主要内容,如果未能解决你的问题,请参考以下文章

朝花夕拾Android自定义View篇之Android事件分发及传递机制

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

android想要利用多点触控放大或者缩小一个view该怎么实现

自定义圆形图片实现多点触控放大缩小和拖动

Android自定义ImageView实现图片缩放滑动,双击放大缩小,多点触控缩放

Android自定义ImageView实现图片缩放滑动,双击放大缩小,多点触控缩放