Android触摸事件--MotionEvent

开篇

最近在研究自定义View方面的知识。而自定义View中很重要的一块就是View的交互。这就牵涉到本系列文章要讲到的Android 触摸事件相关的知识。

而手指每次与屏幕的交互都会由gesture产生一系列的事件:由MotionEvent对象来表述。

MotionEvent 包含的信息

MotionEvent是用于描述一个运动事件(鼠标、笔、手指、轨迹球)的类。MotionEvent可能持有绝对或者相对运动和其他数据,这取决于设备的类型。

1、坐标

如开篇所述,每个触摸事件都代表用户在屏幕上的一个动作,而每个动作必定有其发生的位置。
在MotionEvent中有两组可以获得触摸位置的函数

event.getX(); //触摸点相对于View左上角为原点坐标系的X坐标
event.getY(); //触摸点相对于View左上角为原点坐标系的Y坐标
event.getRawX(); //触摸点相对于屏幕左上角为原点坐标系的X坐标
event.getRawY(); //触摸点相对于屏幕左上角为原点坐标系的Y坐标
触摸坐标信息.png
 final float offsetX = mScrollX - child.mLeft;//子View相对于父View坐标系的偏移值-X
                    final float offsetY = mScrollY - child.mTop;//子View相对于父View坐标系的偏移值-Y
                    event.offsetLocation(offsetX, offsetY);//保证传递给子View时,event的getX()、getY() 是相对于该子View的坐标系的坐标值。

                    handled = child.dispatchTouchEvent(event);//子View分发事件

                    event.offsetLocation(-offsetX, -offsetY);

这段代码清晰展示了父视图把事件分发给子视图时,getX()和getY所获得的相关坐标是如何改变的。当父视图处理事件时,上述两个函数获得的相对坐标是相对于父视图的,当需要将该事件分发给子视图时,就通过上边这段代码,调整了相对坐标的值,让其变为相对于子视图。

2、事件类型

action code能引起View状态的变化,比如手指向上滑动、向下滑动。

设计MotionEvent的事件类型主要有:

    public static final int ACTION_DOWN             = 0;
    public static final int ACTION_UP               = 1;
    public static final int ACTION_MOVE             = 2;
    public static final int ACTION_CANCEL           = 3;
    public static final int ACTION_OUTSIDE          = 4;
    public static final int ACTION_POINTER_DOWN     = 5;
    public static final int ACTION_POINTER_UP       = 6;
  • ACTION_DOWN: 第一个手指按下时
  1. ACTION_MOVE:按住一点在屏幕上移动
  2. ACTION_UP:最后一个手指抬起时
  3. ACTION_CANCEL:当前的手势被取消了,并且再也不会接收到后续的触摸事件,这时我们就像ACTION_UP一样对待他以结束该手势操作,但是却不执行我们在ACTION_UP时需要执行的动作。
    要理解这个类型,就必须要了解ViewGroup分发事件的机制。一般来说,如果一个子视图接收了父视图分发给它的ACTION_DOWN事件,那么与ACTION_DOWN事件相关的事件流就都要分发给这个子视图,但是如果父视图希望拦截其中的一些事件,不再继续转发事件给这个子视图的话,那么就需要给子视图一个ACTION_CANCEL事件。这在后续文章中源码分析部分也有体现。
  4. ACTION_OUTSIDE: 表示用户触碰超出了正常的UI边界.
  5. ACTION_POINTER_DOWN:代表用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,又新出现了一个触摸点。
  6. ACTION_POINTER_UP::代表用户的一个手指离开了触摸屏,但是还有其他手指还在触摸屏上。也就是说,在多个触摸点存在的情况下,其中一个触摸点消失了。它与ACTION_UP的区别就是,它是在多个触摸点中的一个触摸点消失时(此时,还有触摸点存在,也就是说用户还有手指触摸屏幕)产生,而ACTION_UP可以说是最后一个触摸点消失时产生。会在多指触摸和Pointers章节详解。

3、其他属性

getEdgeFlags():当事件类型是ActionDown时可以通过此方法获得,手指触控开始的边界. 如果是的话,有如下几种值:

  public static final int EDGE_TOP = 0x00000001;
  public static final int EDGE_BOTTOM = 0x00000002;
  public static final int EDGE_LEFT = 0x00000004;
  public static final int EDGE_RIGHT = 0x00000008;

EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM

单点手势操作

当我们在操作手机屏幕时,哪怕只是轻轻点击一下,系统也会产生一系列的触摸事件(MotionEvent)对象。具体都会有哪些对象产生和我们的操作密不可分。这个动作所产生的一系列事件,被称为一个事件流,通常包括一个ACTION_DOWN事件,很多个ACTION_MOVE事件,和一个ACTION_UP事件。
轻轻点击一下:

com.zlq.customwidget V/TouchEventTest: 触摸手势:0--ACTION_DOWN
com.zlq.customwidget V/TouchEventTest: 触摸手势:1--ACTION_UP

点击按下后滑动一下再抬起:

com.zlq.customwidget V/TouchEventTest: 触摸手势:0--ACTION_DOWN
com.zlq.customwidget V/TouchEventTest: 触摸手势:2--ACTION_MOVE
com.zlq.customwidget V/TouchEventTest: 触摸手势:2--ACTION_MOVE
...
com.zlq.customwidget V/TouchEventTest: 触摸手势:1--ACTION_UP

我们在使用手势判断时一般如下:

  int action = MotionEventCompat.getAction(event);
    switch(action) {
    case MotionEvent.ACTION_DOWN:
      //按下时
       Log.v("TouchEventTest","触摸手势:"+action+"--"+"ACTION_DOWN");
      break;
    case MotionEvent.ACTION_MOVE:
      //移动时
      Log.v("TouchEventTest","触摸手势:"+action+"--"+"ACTION_MOVE");
      break;
    case MotionEvent.ACTION_UP:
      //离开时
      Log.v("TouchEventTest","触摸手势:"+action+"--"+"ACTION_UP");
      break;
    case MotionEvent.ACTION_CANCEL:
      //取消时
      Log.v("TouchEventTest","触摸手势:"+action+"--"+"ACTION_CANCEL");
      break;

多指手势操作和Pointers

为了可以表示多点触摸的的动作,MotionEvent中引入了Pointer的概念,多点触摸时每个手指产生一个运动轨迹。产生运动轨迹的各个手指或其他物体被称为pointers

  • 一个MotionEvent对象中可能会存储多个pointer的相关信息
  • 每个pointer都有自己的事件类型,也有自己的横轴坐标值。
  • 每个pointer都会有一个自己的id和index。
    • pointer的id在整个事件流中是不会发生变化的,但是index会发生变化。所以,当我们要记录一个触摸点的事件流时,就只需要保存其id,然后使用findPointerIndex(int)来获得其index值,然后再获得其他信息。
    • MotionEvent类中的很多方法都是可以传入一个int值作为参数的,其实传入的就是pointer的index值。比如getX(pointerIndex)和getY(pointerIndex),此时,它们返回的就是index所代表的触摸点相关事件坐标值。

那么,用户先两个手指先后接触屏幕,同时滑动,然后在先后离开这一套动作所产生的事件流是什么样的呢?
 它所产生的事件流如下:

  1. 先产生一个ACTION_DOWN事件,代表用户的第一个手指接触到了屏幕。
  • 再产生一个 ACTION_POINTER_DOWN 事件,代表用户的第二个手指接触到了屏幕。
  • 很多的 ACTION_MOVE 事件,但是在这些MotionEvent对象中,都保存着两个触摸点滑动的信息,相关的代码我们会在文章的最后进行演示。
  • 一个 ACTION_POINTER_UP 事件,代表用户的一个手指离开了屏幕。
  • 如果用户剩下的手指还在滑动时,就会产生很多ACTION_MOVE事件。
  • 一个 ACTION_UP 事件,代表用户的最后一个手指离开了屏幕

跟单点手势操作一样,多点手势操作的信息也是包含在方法onTouchEvent的MotionEvent参数内,MotionEvent中有如下函数可以获取多点触摸信息:

int getPointerCount() //手势操作所包含的点的个数
int findPointerIndex(int pointerId) //根据pointerId找到pointer在MotionEvent中的index
int getPointerId(int pointerIndex) //根据MotionEvent中的index返回pointer的唯一标识
float getX(int pointerIndex) //返回手势操作点的x坐标
float getY(int pointerIndex) //返回手势操作点的y坐标
final int getActionMasked () //获取特殊点的action 
final int getActionIndex()//  用来获取当前按下/抬起的点的标识。如果当前没有任何点抬起/按下,该函数返回0。比如事件类型为ACTION_MOVE时,该值始终为0。

获取事件类型 getAction 和 getActionMasked

从上一节我们可以得知,一个MotionEvent对象中可以包含多个触摸点的事件。当MotionEvent对象只包含一个触摸点的事件时,上边两个函数的结果是相同的,但是当包含多个触摸点时,二者的结果就不同了。
 getAction获得的int值是由pointer的index值和事件类型值组合而成的,而getActionWithMasked则只返回事件的类型值。

getAction返回结果中不同位所代表的含义:前8位代表id,后8位代表事件类型。

我们看下面解析&例子:

MotionEvent类中定义了两个ACTION的掩码:

public static final int ACTION_MASK             = 0xff;
public static final int ACTION_POINTER_INDEX_MASK  = 0xff00;

转化为二进制就是:

ACTION_MASK =               0000000011111111
ACTION_POINTER_INDEX_MASK = 1111111100000000

假设我们操作时getAction()返回0x0105,转化为二进制就是

getAction()   =             0000000100000101

这时,

int indexOriginal = getAction() & ACTION_POINTER_INDEX_MASK = 0000000100000000;
int index=indexOriginal >> 8 ;//就得到了pointer的index:00000001,即1.
int action=  getAction() & ACTION_MASK =0000000000000101 ;//就得到了pointer的真正action,即5,即ACTION_POINTER_DOWN。

而getActionMasked()就会直接返回5,和以上getAction() & ACTION_MASK运算后结果一样。
以上例子中的推断从MotionEvent的源码中也可以得到佐证:

    public static final int ACTION_MASK                = 0xff;
    public static final int ACTION_POINTER_INDEX_MASK  = 0xff00;
    public static final int ACTION_POINTER_INDEX_SHIFT = 8;

    public final int getAction() {
        return nativeGetAction(mNativePtr);
    }
    public final int getActionMasked() {
        return nativeGetAction(mNativePtr) & ACTION_MASK;
    }
    public final int getActionIndex() {
        return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK)
                >> ACTION_POINTER_INDEX_SHIFT;
    }

对于ACTION_DOWN、ACTION_UP之间的其他点(包括ACTION_POINTER_DOWN、ACTION_MOVE、ACTION_POINTER_UP),Android称之为maskedAction,可以使用函数public final int getActionMasked()来查询这个动作是ACTION_POINTER_DOWN、ACTION_POINTER_UP还是ACTION_MOVE。

批处理

为了效率,Android系统在处理ACTION_MOVE事件时会将连续的几个多触点移动事件打包到一个MotionEvent对象中。我们可以通过getX(int)和getY(int)来获得最近发生的一个触摸点事件的坐标,然后使用getHistorical(int,int)和getHistorical(int,int)来获得时间稍早的触点事件的坐标,二者是发生时间先后的关系。所以,我们应该先处理通过getHistoricalXX相关函数获得的事件信息,然后在处理当前的事件信息。
 下边就是Android Guide中相关的例子:

 void printSamples(MotionEvent ev) {
       final int historySize = ev.getHistorySize();
       final int pointerCount = ev.getPointerCount();
       for (int h = 0; h < historySize; h++) {
           System.out.printf("At time %d:", ev.getHistoricalEventTime(h));
           for (int p = 0; p < pointerCount; p++) {
               System.out.printf("  pointer %d: (%f,%f)",
                   ev.getPointerId(p), ev.getHistoricalX(p, h), ev.getHistoricalY(p, h));
           }
       }
       System.out.printf("At time %d:", ev.getEventTime());
       for (int p = 0; p < pointerCount; p++) {
           System.out.printf("  pointer %d: (%f,%f)",
               ev.getPointerId(p), ev.getX(p), ev.getY(p));
       }
   }

概览
MotionEvent描述交互操作中的action code和坐标值等信息。坐标值信息包含该位置和其他运动信息。例如,当用户第一次触摸屏,该系统提供了一个触摸事件到相应的视图与动作代码ACTION_DOWN 和一组轴值,包括X和触摸和左右的压力,尺寸信息的Y坐标和接触区域的方向。
某些设备支持多点触摸。多点触摸时每个手指产生一个运动轨迹。产生运动轨迹的各个手指或其他物体被称为pointers.MotionEvent 包含所有pointers的信息,就算其中有些已经不再移动了。
pointers的数量一般只会被个别手指的抬起放下所影响,特殊情况是当手势取消时。

参考文献

https://developer.android.com/reference/android/view/MotionEvent.html
http://www.jianshu.com/p/0c863bbde8eb
http://my.oschina.net/banxi/blog/56421

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容