【本文出自大圣代的技术专栏 http://blog.csdn.net/qq_23191031】
【转载烦请注明出处,尊重他人劳动成果就是对您自己的尊重】
前言
在《【Android 控件架构】详解Android控件架构与常用坐标系》这篇文章中,我曾在【常用坐标系】一节中简单描述过 MotionEvent 常用方法,鉴于最近工作中大量处理了View触摸事件,特此展开讨论。
1,MotionEvent
对于 MotionEvent 官网是如此定义的:
Motion events describe movements in terms of an action code and a set of axis values. The action code specifies the state change that occurred such as a pointer going down or up. The axis values describe the position and other movement properties.
译:
运动事件描述了动作的动作代码和一些列的坐标值。动作代码表明了当触点按下或者弹起等引起的状态变化。坐标值描述了位置信息以及以他的运动属性。
官网对于MotionEvent的描述相当准确,但是过于抽象,不要紧,请随我来......
1.1 获取点击事件具体坐标
我在《【Android 控件架构】详解Android控件架构与常用坐标系》第二节【视图坐标系】中有详细描述,这里不再赘述。
贴一张图,答案尽在不言中。
1.2 事件类型
从源码中可以看到,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_UP | 最后一个手指抬起 |
ACTION_MOVE | 按住一点手指开始移动 |
ACTION_CANCEL | 表示手势被取消了,不再接受后续事件 |
ACTION_OUTSIDE | 标志着用户触碰到了正常的UI边界 |
ACTION_POINTER_DOWN | 代表用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,又新出现了一个触摸点。 |
ACTION_POINTER_UP | 非最后一个手指抬起 |
对于ACTION_CANCEL我要特殊说明一下,要很好的理解它就不得不说到 ViewGroup的事件分发机制了。一般来说,一个View接收了其父View/ViewGroup分发而来的ACTION_DOWN事件,那么接下来与ACTION_DOWN相关的事件流都会分发给此View处理,但是如果对于某一个事件,其父View/ViewGroup想要拦截并自行处理,那么父View/ViewGroup就会给子视图一个ACTION_CANCEL事件。对于事件分发的内容我会在以后的文章中作出详细解答。这里先留一个悬念。
多指手势操作和Pointers多点触摸时,我们怎么知道这个一个MotionEvent是哪一个触控点触发的呢,所以Android就引入了 pointers 的概念,多点触控时每一个触控点都会产生一个运动轨迹,而引发这个运动轨迹的手指或者物体(如触控笔等)被称为 pointers。
关于Pointers的一些特性总结
- 一个MotionEtvent对象中可存储多个 pointer
- 每个pointer都有自己的事件类型,也有自己的坐标值。
- 每个pointer都会有一个自己的id和index(下面的内容会提到)
- pointer的id在整个事件流中是不会发生变化的,但是index会发生变化。所以,当我们要记录一个触摸点的事件流时,就只需要保存其id,然后使用findPointerIndex(int)来获得其index值,然后再获得其他信息。
- MotionEvent类中的很多方法都是可以传入一个int值作为参数的,其实传入的就是pointer的index值。比如getX(pointerIndex)和getY(pointerIndex),此时,它们返回的就是index所代表的触摸点相关事件坐标值。
案例讲解
用户先两个手指先后接触屏幕,同时滑动,然后在先后离开。这一套动作所产生的事件流是什么样的呢???
常用事件
与单点触发一样,多点触发的相关信息也存储在 onTouchEvent方法的MotinoEvent中:
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
/*
* 用来获取当前按下/抬起的点的标识,
* 如果当前没有任何点抬起/按下,该函数返回0。比如事件类型为ACTION_MOVE时,该值始终为 0。
*/
final int getActionIndex()
获取事件类型 getAction 和 getActionMasked
先贴上一段源码:
/**
* action码的位掩码部分就是action本身
*/
public static final int ACTION_MASK = 0xff;
/**
* 返回action的类型,考虑使用getActionMasked()和getActionIndex()来获得单独的经过掩码的action和触控点的索引.
* @return action例如ACTION_DOWN或者ACTION_POINTER_DOWN与转换的触控点索引的合成值
*/
public final int getAction() {
return nativeGetAction(mNativePtr);
}
/**
* 返回经过掩码的action,没有触控点索引信息.
* 通过getActionIndex()来得到触控操作点的索引.
* @return action,例如ACTION_DOWN,ACTION_POINTER_DOWN
*/
public final int getActionMasked() {
return nativeGetAction(mNativePtr) & ACTION_MASK;
}
查看源码发现getAction
返回的是一个整数,这就说明了Android系统实质上使用一个32位的整形数表示一个TouchEvent事件,而这两个方法的差异就在于 getActionMasked()
返回的是 getAction()
和 ACTION_MASK
进行&
(按位与)操作后的结果。
ACTION_MASK
十进制为 255
到这里我们可以得知:
getActionMasked()
返回的是getAction()
低8位内容。getAction()
的低8才是动作码,也就是说getActionMasked()
返回的仅仅是动作码
ACTION_POINTER_INDEX_MASK
十进制为: 65280 (0x0000ff00)
public static final int ACTION_POINTER_INDEX_MASK = 0xff00;
public static final int ACTION_POINTER_INDEX_SHIFT = 8;
public final int getActionIndex() {
return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK)
>> ACTION_POINTER_INDEX_SHIFT;
}
到这里我们可以得知:
getAction()
的相对高8位,就是用于区分出发事件顺序的 index
小结
事件或方法 | 描述 |
---|---|
getAction() |
触摸动作的原始32位信息,包括事件的动作,触控点信息。32位中只有后16位被使用,其中相对高8位触控点信息,低8位为事件动作 |
getActionMask() |
只包含触摸的动作,如按下,抬起,滑动,多点按下,多点抬起,如果想处理多点触摸炫需要使用 getActionMask() 与MotionEvent中的ACTION_POINTER_DOWN 、ACTION_POINTER_UP 、ACTION_POINTER_1_DOWN等比对判断 |
单点触控 |
getAction() 和getActionMasked() 返回的是值一样的,都只包含事件动作码 |
多点触控 |
getAction() 和getActionMasked() 返回值不同,getAction() 比getActionMasked() 多了触控点索引 |
总而言之:
对于单点触摸,使用这俩个方法谁都行,但是对于多点触控,推进getActionMask方法,可以直接获取ACTION_POINTER_DOWN等多点触控状态码。
为啥说推动使用 getActionMask 呢,因为getAction包含了全部信息,例如双指按下,如果使用getAction的话,这个值是261,如果使用getActionMasked这个值是5(ACTION_POINTER_DOWN =5),所以不闲麻烦,你完全可以使用 getAction方法 通过261硬编码的方式判断。
提问时间:
为什么Android不用两个字段表示呢?例如 mAction,mPointer mAction表示动作类型,mPointer表示第几个触控点。
答: 因为Action与Pointer都只需要256(0~255)个数就可以表示全,只要一个int
字段(32位),否则需要两个字段(32*2=64位),即可以节约内存。又可以方便提高处理速度。不过通常我们都是以不同的字段来存储不同的信息。但是在计算机内部他们还是变成了0,1。计算机始终还是以位来存储信息的。如果我们多熟悉以 位 为基本单位来理解信息的存储。对于理解android中的很多变量是很有帮助的。因为他其中的很多东西使用的这样的节约内在的技巧。如onMeasure中的MeasureSpec,Color.argb()等方法也是如此。
其他
Android 将所有的输入事件都放在了 MotionEvent 中,随着安卓的不断发展壮大,MotionEvent 也开始变得越来越复杂,下面是我自己整理的 MotionEvent 大事记:
2,ViewConfiguration
在ViewConfiguration这个类中,主要定义了UI中所有使用到的标准常量,像超时、尺寸、距离,如果我们需要得到这些常量的数据,我们就可以通过这个类来获取。
注意:
获取ViewConfiguration对象时,由于ViewConfiguration的构造方法为私有的,只能通过这个静态方法来获取到该对象。
ViewConfiguration configure = ViewConfiguration.get(context);
2.1,TouchSlop
TouchSlop是处理触摸事件中的一个常量,被系统认为滑动和点击事件的临界点。理解这个touchSlop是一个滑动距离值的常量,也就是说当我们手触摸在屏幕上滑动时,如果滑动距离没有超过touchSlop值的话 ,android系统本身是不会认为我们在屏幕上做了手势滑动,因此只有当我们在屏幕上的滑动距离超过touchSlop值时,android系统本身才会认为我们做了滑动操作并去响应触摸事件,不过要注意的是不同的设备,touchSlop的值可能是不同的,一切以函数获取为准。它在各家手机系统默认值是不同的。
在使用时我们可以通过:ViewConfiguration.get(getContext()).getScaledTouchSlop()
获取系统的滑动常量来,判断此时是否属于滑动事件;
其中ViewConfiguration这个类主要定义了UI中所使用到的标准常量,像超时、尺寸、距离。
在处理滑动事件时,其实可以利用这个值来过滤掉一些没必要的动作,比如当两次滑动距离小于这个值时,我们就可以认为滑动没发生,从而更好的优化用户体验。
附上一段常用源码
/**
* 包含了方法和标准的常量用来设置UI的超时、大小和距离
*/
public class ViewConfiguration {
// 设定水平滚动条的宽度和垂直滚动条的高度,单位是像素px
private static final int SCROLL_BAR_SIZE = 10;
//定义滚动条逐渐消失的时间,单位是毫秒
private static final int SCROLL_BAR_FADE_DURATION = 250;
// 默认的滚动条多少秒之后消失,单位是毫秒
private static final int SCROLL_BAR_DEFAULT_DELAY = 300;
// 定义边缘地方褪色的长度
private static final int FADING_EDGE_LENGTH = 12;
//定义子控件按下状态的持续事件
private static final int PRESSED_STATE_DURATION = 125;
//定义一个按下状态转变成长按状态的转变时间
private static final int LONG_PRESS_TIMEOUT = 500;
//定义用户在按住适当按钮,弹出全局的对话框的持续时间
private static final int GLOBAL_ACTIONS_KEY_TIMEOUT = 500;
//定义一个touch事件中是点击事件还是一个滑动事件所需的时间,如果用户在这个时间之内滑动,那么就认为是一个点击事件
private static final int TAP_TIMEOUT = 115;
/**
* Defines the duration in milliseconds we will wait to see if a touch event
* is a jump tap. If the user does not complete the jump tap within this interval, it is
* considered to be a tap.
*/
//定义一个touch事件时候是一个点击事件。如果用户在这个时间内没有完成这个点击,那么就认为是一个点击事件
private static final int JUMP_TAP_TIMEOUT = 500;
//定义双击事件的间隔时间
private static final int DOUBLE_TAP_TIMEOUT = 300;
//定义一个缩放控制反馈到用户界面的时间
private static final int ZOOM_CONTROLS_TIMEOUT = 3000;
/**
* Inset in pixels to look for touchable content when the user touches the edge of the screen
*/
private static final int EDGE_SLOP = 12;
/**
* Distance a touch can wander before we think the user is scrolling in pixels
*/
private static final int TOUCH_SLOP = 16;
/**
* Distance a touch can wander before we think the user is attempting a paged scroll
* (in dips)
*/
private static final int PAGING_TOUCH_SLOP = TOUCH_SLOP * 2;
/**
* Distance between the first touch and second touch to still be considered a double tap
*/
private static final int DOUBLE_TAP_SLOP = 100;
/**
* Distance a touch needs to be outside of a window's bounds for it to
* count as outside for purposes of dismissing the window.
*/
private static final int WINDOW_TOUCH_SLOP = 16;
//用来初始化fling的最小速度,单位是每秒多少像素
private static final int MINIMUM_FLING_VELOCITY = 50;
//用来初始化fling的最大速度,单位是每秒多少像素
private static final int MAXIMUM_FLING_VELOCITY = 4000;
//视图绘图缓存的最大尺寸,以字节表示。在ARGB888格式下,这个尺寸应至少等于屏幕的大小
@Deprecated
private static final int MAXIMUM_DRAWING_CACHE_SIZE = 320 * 480 * 4; // HVGA screen, ARGB8888
//flings和scrolls摩擦力度大小的系数
private static float SCROLL_FRICTION = 0.015f;
/**
* Max distance to over scroll for edge effects
*/
private static final int OVERSCROLL_DISTANCE = 0;
/**
* Max distance to over fling for edge effects
*/
private static final int OVERFLING_DISTANCE = 4;
}
3, VelocityTracker(读音 [vəˈlɑ:səti] ['trækər] )
VelocityTracker是个速度跟踪类,用于跟踪手指滑动的速度,包括x轴方向和y轴方向的速度。如快速滑动或者其他手势操作。当我们准备开始跟踪滑动速率时可以使用obtain()方法来获取一个 VelocityTracker的实例,然后在onTouchEvent回调函数中,使用addMovement(MotionEvent)函数将当前的 移动事件传递给VelocityTracker对象。当我们决定计算当前触摸点的速率时可以调用computeCurrentVelocity(int units)函数来计算当前的速度,使用getXVelocity() 、getYVelocity()函数来获得当前X轴和Y轴的速度。
使用实例
@Override
public boolean onTouchEvent(MotionEvent event) {
LogUtils.e("onTouchEvent start!!");
Log.i(TAG, "ACTION_DOWN");
if(null == mVelocityTracker) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
final VelocityTracker verTracker = mVelocityTracker;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//获取第一个触点的id, 此时可能有多个触点,获取其中一个
mPointerId = event.getPointerId(0);
break;
case MotionEvent.ACTION_MOVE:
//计算瞬时速度
verTracker.computeCurrentVelocity(1000, mMaxVelocity);
float velocityX = verTracker.getXVelocity(mPointerId);
float velocityY = verTracker.getYVelocity(mPointerId);
LogUtils.e("velocityX-->" + velocityX);
LogUtils.e("velocityY-->"+velocityY);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
releaseVelocityTracker();//释放资源
break;
default:
break;
}
return super.onTouchEvent(event);
}
/**
* 使用完VelocityTracker,必须释放资源
*/
private void releaseVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
4,鸣谢:
如果说我比别人看得更远些,那是因为我站在了巨人的肩上
android触控,先了解MotionEvent(一)
Android触摸事件--MotionEvent
ViewConfiguration解析
【大圣代的技术专栏 http://blog.csdn.net/qq_23191031 转载烦请注明出处,尊重他人劳动成功就是对您自己的尊重】