Android MotionEvent

1.MotionEvent
Android将所有的输入事件都放在了 MotionEvent 中,MotionEvent 负责集中处理所有类型设备的输入事件,包括单点触控、手势、多点触控、触控笔、鼠标、键盘、操纵杆、游戏控制器等。

事件类型:
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:不是由用户直接触发,而是由系统在需要的时候触发,例如子view接收到了down事件,但是在move的时候,父view通过onInterceptTouchEvent()返回true,拦截了该事件流,从子view拿回了处理事件的控制权,这时候子view就会收到一个ACTION_CANCEL事件,并且子view再也不会收到后续事件了,后续事件都交给父view来处理了。
简单来说,根据ViewGroup分发事件的机制,一般来说,如果一个子视图接收了父视图分发给它的ACTION_DOWN事件,那么与ACTION_DOWN事件相关的事件流就都要分发给这个子视图,但是如果父视图希望拦截其中的一些事件,不再继续转发事件给这个子视图的话,那么就需要给子视图一个ACTION_CANCEL事件。
⑤ACTION_OUTSIDE:表示用户触碰超出了正常的UI边界。
官方解释:A movement has happened outside of the normal bounds of the UI element. This does not provide a full gesture, but only the initial location of the movement/touch.
一个触摸事件在UI元素的正常范围之外发生。因此不再提供完整的手势,只提供 运动/触摸 的初始位置。
正常情况下,如果初始点击位置在该视图区域之外,该视图根本不可能会收到事件。然而,总有一些特殊情况,比如点击 Dialog 区域外关闭,Dialog就是一个特殊的视图(没有占满屏幕大小的窗口),能够接收到视图区域外的事件(虽然在通常情况下你根本用不到这个事件)。
当然,想要接收到视图之外的事件需要一些特殊的设置,即设置视图的 WindowManager 布局参数的 flags为FLAG_WATCH_OUTSIDE_TOUCH ,这样点击事件发生在这个视图之外时,该视图就可以接收到一个 ACTION_OUTSIDE事件。
⑥ACTION_POINTER_DOWN:用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,又新出现了一个触摸点。
⑦ACTION_POINTER_UP:用户的一个手指离开了触摸屏,但是还有其他手指还在触摸屏上。也就是说,在多个触摸点存在的情况下,其中一个触摸点消失了。
它与ACTION_UP的区别就是,它是在多个触摸点中的一个触摸点消失时产生,而ACTION_UP是最后一个触摸点消失时产生。
⑧MotionEvent.ACTION_SCROLL:android3.1引入,非触摸滚动,主要是由鼠标、滚轮、轨迹球触发。

常用方法:
①getAction():返回动作类型
②getX()/getY():事件发生时,触摸的中间区域的X/Y坐标,由这两个函数获得的X/Y值是相对坐标,相对于消费这个事件的视图的左上角的坐标。
③getRawX()/getRawY():由这两个函数获得的X/Y值是绝对坐标,是相对于屏幕的。
在这里插入图片描述
④getSize():指压范围,获取第一个手指与屏幕接触面积的大小
getSize(int pointerIndex):获取第pointerIndex个手指与屏幕接触面积的大小
⑤getPressure():获取第一个手指的压力大小,0-1之间,看情况,很可能始终返回1,具体的级别由驱动和物理硬件决定的
getPressure(int pointerIndex):获取第pointerIndex个手指的压力大小
⑥getEdgeFlags():当事件类型是ActionDown时可以通过此方法获得边缘标记(EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM),但是看设备情况,很可能始终返回0
⑦getDownTime() :按下开始时间
⑧getEventTime() : 事件结束时间
⑨getActionMasked():多点触控获取经过掩码后的动作类型
⑩getActionIndex():多点触控获取经过掩码和平移后的索引
⑩getPointerCount():获取触控点的数量,比如2则可能是两个手指同时按压屏幕
⑩getPointerId(int pointerIndex):获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
⑩findPointerIndex(int pointerId):通过pointerId获取到当前状态下PointerIndex,之后可以通过PointerIndex获取其他内容。
⑩getX(int pointerIndex):获取第PointerIndex个触控点的x位置
⑩getY(int PointerIndex):获取第PointerIndex个触控点的y位置
⑩getPressure(nID):获取第nID个触控点的压力

动作类型和动作索引:
单点触控时用8位二进制数代表动作类型,如0x01,这时getAction返回的值就是ACTION_UP。
多点触控时因为增加了本次触摸的索引,所以改用16位二进制数,如0x0001,低8位代表动作的类型,高8位代表索引。这时获取动作类型就需要用掩码盖掉高8位,而获取索引需要用掩码盖掉低8位然后再右移8位,如下:
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 getActionMasked() {
return mAction & ACTION_MASK;
}
public final int getActionIndex() {
return (mAction & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT;
}

2.getAction() 与 getActionMasked()
当多个手指在屏幕上按下的时候,会产生大量的事件,如何在获取事件类型的同时区分这些事件呢?
一般来说我们可以通过为事件添加一个int类型的index属性来区分,但是我们知道谷歌工程师是有洁癖的,为了添加一个数值不会超过10的index属性就浪费一个int大小的空间简直是不能忍受的,于是工程师们将这个index属性和事件类型直接合并了。
int类型共32位(0x00000000),他们用最低8位(0x000000ff)表示事件类型,再往前的8位(0x0000ff00)表示事件编号。

以手指按下为例讲解数值是如何合成的:
ACTION_DOWN 的默认数值为 (0x00000000);
ACTION_POINTER_DOWN 的默认数值为 (0x00000005)。

第1个手指按下,触发事件ACTION_DOWN (0x00000000);
第2个手指按下,触发事件ACTION_POINTER_DOWN (0x00000105);
第3个手指按下,触发事件ACTION_POINTER_DOWN (0x00000205);
第4个手指按下,触发事件ACTION_POINTER_DOWN (0x00000305);

注意:
可以看到随着按下手指数量的增加,这个数值的高8位也是一直变化的,进而导致我们使用 getAction() 获取到的数值无法与标准的事件类型进行对比。为了解决这个问题,他们创建了一个 getActionMasked() 方法,这个方法可以清除index数值,让其变成一个标准的事件类型。
①多点触控时必须使用 getActionMasked() 来获取事件类型。
②单点触控时由于事件数值不变,使用 getAction() 和 getActionMasked() 两个方法都可以。
③使用 getActionIndex() 可以获取到这个index数值。不过要注意,getActionIndex() 只在 down 和 up 时有效,move 时是无效的。

目前来说获取事件类型使用 getActionMasked() 就行了,但是如果一定要编译时兼容古董版本的话,可以考虑使用这样的写法:
final int action = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO)
? event.getActionMasked()
: event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
//TODO
break;
}

3.Pointer
当用户两个或者多个手指在屏幕上滑动时,为了可以表示多个触摸点的动作,MotionEvent中引入了Pointer的概念,一个pointer就代表一个触摸点,每个pointer都有自己的事件类型,也有自己的横轴坐标值。一个MotionEvent对象中可能会存储多个pointer的相关信息,每个pointer都会有一个自己的id和index。pointer的id在整个事件流中是不会发生变化的,但是index会发生变化。

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

PointId:
追踪事件流,一定要用 PointId,这是唯一官方指定标准,不要使用 ActionIndex 。
PointId 在手指按下时产生,手指抬起或者事件被取消后消失,是一个事件流程中唯一不变的标识,可以在手指按下时,通过 getPointerId(int pointerIndex) 获得 (参数中的 pointerIndex 就是 actionIndex)。
在一次运动中,触点出现的顺序是不确定的,因此,触点的索引值会由于事件的变化而变化,但是只要触点处于活动状态,该触点的id就不会改变。用getPointId(int)方法可以得到触点的id值,从而根据得到的id值在一连串的动作中来追踪其运动轨迹。然而在连续的运动事件中,应该用findPointIndex(int)方法通过触点的id值得到触点的索引值。
private final static int INVALID_ID = -1;
private int mActivePointerId = INVALID_ID;
private int mSecondaryPointerId = INVALID_ID;
private float mPrimaryLastX = -1;
private float mPrimaryLastY = -1;
private float mSecondaryLastX = -1;
private float mSecondaryLastY = -1;
public boolean onTouchEvent(MotionEvent event) {
int action = MotionEventCompat.getActionMasked(event);
switch (action) {
case MotionEvent.ACTION_DOWN:
int index = event.getActionIndex();
mActivePointerId = event.getPointerId(index);
mPrimaryLastX = MotionEventCompat.getX(event,index);
mPrimaryLastY = MotionEventCompat.getY(event,index);
break;
case MotionEvent.ACTION_POINTER_DOWN:
index = event.getActionIndex();
mSecondaryPointerId = event.getPointerId(index);
mSecondaryLastX = event.getX(index);
mSecondaryLastY = event.getY(index);
break;
case MotionEvent.ACTION_MOVE:
index = event.findPointerIndex(mActivePointerId);
int secondaryIndex = MotionEventCompat.findPointerIndex(event,mSecondaryPointerId);
final float x = MotionEventCompat.getX(event,index);
final float y = MotionEventCompat.getY(event,index);
final float secondX = MotionEventCompat.getX(event,secondaryIndex);
final float secondY = MotionEventCompat.getY(event,secondaryIndex);
break;
case MotionEvent.ACTION_POINTER_UP:
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mActivePointerId = INVALID_ID;
mPrimaryLastX =-1;
mPrimaryLastY = -1;
break;
}
return true;
}

4.MotionEvent回收
MotionEvent类中有个recycle()方法,但是不要通过此方法对MotionEvent对象进行回收。如果回调方法没有使用MotionEvent对象,并且返回了false,MotionEvent对象可能会被传递到其他某个方法、视图或我们的活动。
MotionEvent类中还有和obtain()方法,通过此方法可以创建一个MotionEvent的副本或者全新的MotionEvent,这个副本或全新的事件对象是在完成之后应该回收的对象。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值