开篇
最近在研究自定义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坐标
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
: 第一个手指按下时
-
ACTION_MOVE
:按住一点在屏幕上移动 -
ACTION_UP
:最后一个手指抬起时 -
ACTION_CANCEL
:当前的手势被取消了,并且再也不会接收到后续的触摸事件,这时我们就像ACTION_UP
一样对待他以结束该手势操作,但是却不执行我们在ACTION_UP
时需要执行的动作。
要理解这个类型,就必须要了解ViewGroup分发事件的机制。一般来说,如果一个子视图接收了父视图分发给它的ACTION_DOWN
事件,那么与ACTION_DOWN
事件相关的事件流就都要分发给这个子视图,但是如果父视图希望拦截其中的一些事件,不再继续转发事件给这个子视图的话,那么就需要给子视图一个ACTION_CANCEL
事件。这在后续文章中源码分析部分也有体现。 -
ACTION_OUTSIDE
: 表示用户触碰超出了正常的UI边界. -
ACTION_POINTER_DOWN
:代表用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,又新出现了一个触摸点。 -
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所代表的触摸点相关事件坐标值。
那么,用户先两个手指先后接触屏幕,同时滑动,然后在先后离开这一套动作所产生的事件流是什么样的呢?
它所产生的事件流如下:
- 先产生一个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
//www.greatytc.com/p/0c863bbde8eb
http://my.oschina.net/banxi/blog/56421