【Android View事件(一)】Android常用触控类分析:MotionEvent 、 ViewConfiguration、VelocityTracker

【Android View事件(一)】Android常用触控类分析:MotionEvent 、 ViewConfiguration、VelocityTracker_第1张图片

【本文出自大圣代的技术专栏 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控件架构与常用坐标系》第二节【视图坐标系】中有详细描述,这里不再赘述。

贴一张图,答案尽在不言中。


【Android View事件(一)】Android常用触控类分析:MotionEvent 、 ViewConfiguration、VelocityTracker_第2张图片

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所代表的触摸点相关事件坐标值。

案例讲解

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

【Android View事件(一)】Android常用触控类分析:MotionEvent 、 ViewConfiguration、VelocityTracker_第3张图片

常用事件
与单点触发一样,多点触发的相关信息也存储在 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

【Android View事件(一)】Android常用触控类分析:MotionEvent 、 ViewConfiguration、VelocityTracker_第4张图片
ACTION_MASK二进制表示

到这里我们可以得知:

  • 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;
    }
【Android View事件(一)】Android常用触控类分析:MotionEvent 、 ViewConfiguration、VelocityTracker_第5张图片
ACTION_POINTER_INDEX_MASK二进制表示

到这里我们可以得知:

  • getAction() 的相对高8位,就是用于区分出发事件顺序的 index

小结

事件或方法 描述
getAction() 触摸动作的原始32位信息,包括事件的动作,触控点信息。32位中只有后16位被使用,其中相对高8位触控点信息,低8位为事件动作
getActionMask() 只包含触摸的动作,如按下,抬起,滑动,多点按下,多点抬起,如果想处理多点触摸炫需要使用 getActionMask() 与MotionEvent中的ACTION_POINTER_DOWNACTION_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 大事记:

【Android View事件(一)】Android常用触控类分析:MotionEvent 、 ViewConfiguration、VelocityTracker_第6张图片
image.png

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 转载烦请注明出处,尊重他人劳动成功就是对您自己的尊重】

你可能感兴趣的:(【Android View事件(一)】Android常用触控类分析:MotionEvent 、 ViewConfiguration、VelocityTracker)