• 【朝花夕拾】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                 + ";
     viewX=" + viewX + ";viewY=" + viewY
    23                 + ";
     eventX=" + eventX + ";eventY=" + eventY
    24                 + ";
     rawX=" + rawX + ";rawY=" + rawY
    25                 + ";
     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来加强理解。同样,如果本文有描述不妥或者不准确的地方,欢迎来拍砖,感谢!

  • 相关阅读:
    关于SDK-manager中我们需要下载哪些?
    不只是撸代码搞鸡汤,也有故事!
    [Selenium]如何通过Selenium实现Ctrl+click,即按住Ctrl的同时进行单击操作
    【设计模式】单例模式
    【Java多线程】线程池学习
    【leetcode】147 Insertion Sort List
    【webssh】shellinabox搭建
    【SpringMVC】一次处理项目中文乱码的经历
    【Java多线程】JUC包下的工具类CountDownLatch、CyclicBarrier和Semaphore
    【leetcode】3 SUM
  • 原文地址:https://www.cnblogs.com/andy-songwei/p/11155259.html
Copyright © 2020-2023  润新知