Android手势识别,MotionEvent

   <h2><strong>摘要</strong></h2>    <p>本文是手势识别输入事件处理的完整学习记录。内容包括输入事件InputEvent响应方式,触摸事件MotionEvent的概念和使用,触摸事件的动作分类、多点触摸。根据案例和API分析了触摸手势Touch Gesture的识别处理的一般过程。介绍了相关的GestureDetector,Scroller和VelocityTracker。最后分析drag和scale等一些手势的识别。</p>    <h2><strong>输入源分类</strong></h2>    <p>虽然android本身是一个完整的系统,它主要运行在移动设备的特性决定了我们在它上面开的app绝大数属于客户端程序,主要目标就是显示界面处理交互,这点和web前端以及桌面上的应用类似。</p>    <p>作为“客户端程序”,编写的大部分功能就是处理用户交互。不同系统(对应不同设备)可支持的用户交互各有不同。</p>    <p>android可以运行在多种设备,从交互输入上看, InputDevice.SOURCE_CLASS_xxx 常量标识了sdk所支持的几种不同输入源的设备。有:触屏,物理/虚拟按键,摇杆,鼠标等,下面的讨论针对最广泛的交互——触屏( SOURCE_TOUCHSCREEN)。</p>    <p>触屏设备从交互设计上看就是各种手势,有点击,双击,滑动,拖拽,缩放等等交互定义,本质上它们都是基础的几种触摸事件的不同模式的组合。</p>    <p>在安卓触屏系统中,支持单点、多点(点通常就是手指)触摸,每个点有按下,移动和抬起。</p>    <p>触屏交互的处理分不同触屏操作——手势的识别,然后是根据业务对应不同处理。为了响应不同的手势,首先就需要识别它们。识别过程就是跟踪收集系实时提供的反应用户在屏幕上的动作的"基本事件",然后根据这些数据(事件集合)来判定出各种不同种类的高级别的“动作”。</p>    <p>android.view.GestureDetector提供了对onScroll、onLongPress、onFling等几个最常见动作的监听。而自己的app根据需要可以通过实现自己的GestureDetector类型来识别出类似Drag、Scale这样的交互动作。</p>    <p>手势识别是智能手机和平板等触屏设备的主流交互/输入方式,不同于PC上的键盘和鼠标。</p>    <h2><strong>输入事件</strong></h2>    <p>用户交互产生的输入事件最终由InputEvent的子类来表示,目前包括KeyEvent(Object used to report key and button events)和MotionEvent(Object used to report movement (mouse, pen, finger, trackball) events.)。</p>    <p>接收InputEvent的地方有很多,根据框架对事件的传播路径依次有Activity、Window、View(ViewTree的一条路径:view stack)。</p>    <p>多数情况下都是在用户交互的具体View中接收并处理这些输入事件。</p>    <p>View的事件处理有2种方式,一种是添加监听器(event listener),另一种是重写处理器方法( event handler)。前者比较方便,后者在自定义View时根据需要去重写,而且CustomView也可以根据需要定义自己的处理器方法,或提供监听接口。</p>    <h2><strong>事件监听</strong></h2>    <p>事件监听接口都是只包含一个方法的interface,如:</p>    <pre>  <code class="language-java">// 在View.java中  public interface OnTouchListener {    boolean onTouch(View v, MotionEvent event);  }    public interface OnLongClickListener {        boolean onLongClick(View v);  }    public interface OnClickListener {    void onClick(View v);  }    public interface OnKeyListener {    boolean onKey(View v, int keyCode, KeyEvent event);  }</code></pre>    <p>在Activity等地方通过创建匿名类或实现对应接口(省去新类型和对象的分配)然后调用</p>    <p>View.setOn...Listener()来完成注册监听。</p>    <p>根据android的ui-events(输入事件)的传递机制,监听器的回调方法会先于各种相应的处理器方法被执行,对于那些有返回boolean值的回调方法,返回值表示是否让事件继续被传播,所以应该根据需要谨慎设计返回值,否则会阻塞其它处理的执行。</p>    <p>例如,当为View设置OnTouchListener之后,若回调方法onTouch返回true,那么在View的 boolean dispatchTouchEvent(MotionEvent event) 中执行了回调方法后,就不再执行View中的处理器方法 boolean onTouchEvent(MotionEvent event) 。</p>    <h2><strong>事件处理器</strong></h2>    <p>事件处理器就是在“事件传递”经过当前View时调用的默认方法。通常也就是对应具体View的行为逻辑的实现(要知道监听器不是必须的,甚至可以不去定义,而任何View都会为感兴趣的事件提供处理)。</p>    <p>有关消息传递的知识可以写一整篇了,这里略过,只需要知道,输入事件会沿着ViewTree自顶向下穿过许多“相关的”View,然后这些View处理或继续传递事件。事件到达ViewTree之前还会经过Activity和Window,最终的起源当然是系统负责收集的硬件事件,从“事件管理器”发送给交互中的界面相关的某个类,开始传播。</p>    <p>View类中包括下面的事件处理方法:</p>    <ul>     <li>onKeyDown(int, KeyEvent) - Called when a new key event occurs.</li>     <li>onKeyUp(int, KeyEvent) - Called when a key up event occurs.</li>     <li>onTrackballEvent(MotionEvent) - Called when a trackball motion event occurs.</li>     <li>onTouchEvent(MotionEvent) - Called when a touch screen motion event occurs.</li>     <li>onFocusChanged(boolean, int, Rect) - Called when the view gains or loses focus.</li>    </ul>    <p>上面的处理器方法是站在事件传播管道的当前节点来进行处理的,也就是处理只需要考虑当前View所提供的功能逻辑,并告知调用者是否已经处理结束——需要继续传递?而对于ViewGroup类,它还承担传递事件给childView的任务,下面的方法和事件传递密切相关:</p>    <ul>     <li>Activity.dispatchTouchEvent(MotionEvent) - This allows your Activity to intercept all touch events before they are dispatched to the window.</li>     <li>ViewGroup.onInterceptTouchEvent(MotionEvent) - This allows a ViewGroup to watch events as they are dispatched to child Views.</li>     <li>ViewParent.requestDisallowInterceptTouchEvent(boolean) - Call this upon a parent View to indicate that it should not intercept touch events with onInterceptTouchEvent(MotionEvent).</li>    </ul>    <p>了解在哪些地方可以接收事件,什么时候去处理消耗事件是界面编程的一个重要方面,但“输入事件的传递过程”是一个重要且够复杂的话题,本篇文章重点是触屏事件的各种手势识别,相关的知识仅从“理解的完整和条理性”出发占据一定篇幅。</p>    <h2><strong>TouchMode</strong></h2>    <p>对于触屏设备,用户开始触摸直到离开屏幕(press->lift)期间,界面会处于TouchMode的交互状态。大致来看,所有的View都在响应触摸事件或者其它的KeyEvent(按键,按钮等)事件。两者在交互上截然不同,触摸模式的状态维护贯穿了整个系统,包括所有的Window和Activity对象(主要就是触摸事件的分发的控制),通过View类的 public boolean isInTouchMode () 方法可以查看当前设备是否处在触摸模式。</p>    <h2><strong>Gestures</strong></h2>    <p>用户手指(一或多个)按下和最终完全离开屏幕的过程为一次触屏操作,每次操作都可归类为不同触摸模式(touch pattern),最终被定义为不同的手势(手势和模式的定义是设计上的,用户在使用任何触屏设备后都会学习到不同的手势),android支持的主要手势有:</p>    <ul>     <li>Touch</li>     <li>Long press</li>     <li>Swipe or drag</li>     <li>Long press drag</li>     <li>Double touch</li>     <li>Double touch drag</li>     <li>Pinch open</li>     <li>Pinch close</li>    </ul>    <p>app需要根据系统提供的API来响应这些手势。</p>    <h2><strong>手势识别过程</strong></h2>    <p>为了实现对手势的响应处理,需要理解触摸事件的表示。而识别手势的具体过程包括:</p>    <ol>     <li>获得触摸事件数据。</li>     <li>分析是否匹配所支持的某个手势。</li>    </ol>    <h2><strong>MotionEvent</strong></h2>    <p>触摸动作触发的输入事件由MotionEvent表示,它实现了Parcelable接口——IPC需求。</p>    <p>目前的设备几乎都支持多点触摸,每个触摸中的手指被当做一个poiner。MotionEvent记录了目前所有处于触摸的poiner,包含它们各自的X,Y坐标,压力,接触区域等信息。</p>    <p>每个手指的按下、移动和抬起都会产生一个事件对象。每个事件对应一个“动作”,由MotionEvent.ACTION_xxx的常量来表示:</p>    <ul>     <li>在第一个手指按下时,触发ACTION_DOWN</li>     <li>后续手指按下时触发ACTION_POINTER_DOWN</li>     <li>任何一个手指的移动触发ACTION_MOVE</li>     <li>非最后一个手指抬起触发ACTION_POINTER_UP</li>     <li>最后离开屏幕时触发ACTION_UP</li>     <li>触摸事件序列被中断时触发ACTION_CANCEL,一般是对应View的parent阻止的,比如触摸超出区域时。</li>    </ul>    <p>每一个手指的down,move和up都会产生事件。出于性能考虑,因为移动过程会产生大量的ACTION_MOVE事件,它们被“批量”发送,也就是一个MotionEvent中将可以包含若干个实际的ACTION_MOVE事件数据,很显然,这些事件都是MOVE动作,而且poiner数量是一样的——任何poiner的加入和去除都引发DOWN、UP事件,这样就不是连续的MOVE事件了。</p>    <p>相比上一个MotionEvent数据,当前MotionEvent的所有数据都是最新的。打包的数据根据时间形成数组,而最新的数据被作为current数据。可以通过 getHistorical 系列方法访问“历史事件”的数据。</p>    <p>下面是获得当前MotionEvent中所有事件的各个poiner的坐标的标准形式:</p>    <pre>  <code class="language-java">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));     }  }</code></pre>    <p>前面提到了,事件具有动作分类,而且每个事件对象中包含所有pointer的相关数据。获得action的方式是:</p>    <pre>  <code class="language-java">action = event.getAction() & MotionEvent.ACTION_MASK;</code></pre>    <ul>     <li> <p>getAction和getActionMasked</p> <p>getAction()返回的int数值内可能包含pointerIndex的信息(这里应该是类似View.MeasureSpec那样利用bit位来提升性能的做法):对应ACTION_POINTER_DOWN和ACTION_POINTER_UP动作,返回值包含了触发UP、DOWN的“当前”pointer的index值,然后可以在方法 getPointerId(int), getX(int), getY(int), getPressure(int), and getSize(int)中作为pointerIndex参数使用。方法 getActionIndex() 就是用来获取其中的pointerIndex。而 getActionMasked() 和上面语句的执行逻辑是一样的——返回不包含pointerIndex的action常量值。对应只有一个手指的情况,显然getAction()和getActionMasked()是一样的,因为返回值本身也没有额外的pointerIndex数据。获得事件动作应该使用getActionMasked——更准确些。</p> </li>    </ul>    <p>获得某个pointer的数据的方式也比较特殊,比如获得各个pointer的X坐标:</p>    <pre>  <code class="language-java">final int pointerCount = ev.getPointerCount();  // p就是pointerIndex  for (int p = 0; p < pointerCount; p++) {     System.out.printf("  pointer %d: (%f,%f)",         ev.getPointerId(p), ev.getX(p), ev.getY(p));  }</code></pre>    <p>在一次手势操作过程中,pointer的数量可能发生变化,每一个pointer在DOWN事件的时候就获得一个关联的id,可以作为它的有效标识,直至UP或CANCEL后(pointerCount变化)。</p>    <p>在单个的MotionEvent对象中, getPointerCount() 返回了处于触摸的pointer的总数,0~getPointerCount()-1的值就是当前所有pointer的pointerIndex。方法 float getX(int pointerIndex) 接收index来获得对应pointer的X坐标值。</p>    <p>类似的,其它接收pointerIndex参数的方法用以获得pointer的其它属性。如果需要关注某个手指的连续动作,比如第一个按下的手指,可以通过方法 int getPointerId(int pointerIndex) 获得pointerIndex的id,记录此id,然后在每个MotionEvent数据检查时通过方法 int findPointerIndex(int pointerId) 得到id在当前MotionEvent数据中对应的pointerIndex,就可以访问连续事件中指定id的pointer的属性了。</p>    <p>最后,MotionEvent的以下方法是经常用到的:</p>    <ul>     <li>long getEventTime() 获得事件发生的时间。</li>     <li>long getDownTime() 获得本次触摸事件序列的第一个——手指按下(ACTION_DOWN)的发生时间。</li>     <li>int getAction() 、 int getActionMasked() 、 int getActionIndex() 、 int getPointerCount() 、 int getPointerId(int pointerIndex) 、 float getX() 、 float getX(int pointerIndex) 等。</li>    </ul>    <h2><strong>接收事件数据</strong></h2>    <p>手势操作产生的一系列MotionEvent对象依次分发出去,传递并经过一些UI相关对象,一般的最终会经过对应的Activity和组成界面的那些和当前触屏相关的View对象——沿着ViewTree从事件所在View向上的各个parent。</p>    <p>在当前界面的Activity中,可以通过重写Activity的 boolean onTouchEvent(MotionEvent event) 方法来接收触摸事件,更多时候,因为View是具体实现UI交互的地方,所以在View的 boolean onTouchEvent(MotionEvent event) 方法中接收事件。</p>    <p>一次触摸操作会发送一系列事件,所以onTouchEvent会被“很多次”调用。</p>    <pre>  <code class="language-java">@Override  public boolean onTouchEvent(MotionEvent event) {      int action = event.getAction() & MotionEvent.ACTION_MASK;      switch (action) {          case MotionEvent.ACTION_DOWN:              Log.d(TAG, "ACTION_DOWN");              return true;          case MotionEvent.ACTION_POINTER_DOWN:              Log.d(TAG, "ACTION_POINTER_DOWN");              return true;          case MotionEvent.ACTION_MOVE:              Log.d(TAG, "ACTION_MOVE");              return true;          case MotionEvent.ACTION_UP:              Log.d(TAG, "ACTION_UP");              return true;          case MotionEvent.ACTION_POINTER_UP:              Log.d(TAG, "ACTION_POINTER_UP");              return true;          case MotionEvent.ACTION_CANCEL:              Log.d(TAG, "ACTION_CANCEL");              return true;          default:              Log.d(TAG, "default: action = " + action);              return super.onTouchEvent(event);      }  }</code></pre>    <p>也可以通过设置监听器来接收触摸事件,这是针对具体的View对象进行的:</p>    <pre>  <code class="language-java">myView.setOnTouchListener(new OnTouchListener() {      public boolean onTouch(View v, MotionEvent event) {          // ... Respond to touch events                 return true;      }  });</code></pre>    <p>需要注意的是,不论识别那种手势操作,ACTION_DOWN动作一定需要返回true,否则按照调用约定,将认为当前处理忽略本次触摸操作的事件序列,后续事件不会收到。</p>    <h2><strong>检测手势</strong></h2>    <p>在重写的onTouch回调方法中根据收到的事件序列就可以判定出各种手势。例如,一个ACTION_DOWN,紧接着是一系列的ACTION_MOVE,然后是ACTION_UP,这样的序列通常就是scroll/drag手势。总的说来,在实现识别手势的逻辑时,需要“精心设计”代码,往往需要考虑多少偏移才被当做有效滑动,多少时间间隙的down、up才算tap。 android.view.GestureDetector 提供了对最常见的手势的识别。下面分别对手势识别的关键相关类型做介绍。</p>    <h2><strong>GestureDetector</strong></h2>    <p>它的作用就是识别onScroll、onFling onDown(), onLongPress()等操作。将收到的MotionEvent序列传递给GestureDetector,之后它触发对应不同手势的回调方法。</p>    <p>使用过程为:</p>    <ol>     <li>准备GestureDetector对象,提供响应各种手势回调方法的监听器。OnGestureListener就是对不同手势的回调接口,很好理解。</li>    </ol>    <pre>  <code class="language-java">// public GestureDetector(Context context, OnGestureListener listener);  mDetector = new GestureDetector(this, mGestureListener);</code></pre>    <ol>     <li>在onTouch方法中将收到的事件传递给GestureDetector。</li>    </ol>    <pre>  <code class="language-java">@Override  public boolean onTouchEvent(MotionEvent event) {     boolean handled = mDetector.onTouchEvent(event);     return handled || super.onTouchEvent(event);  }</code></pre>    <p>如果只对GestureDetector的个别手势的回调感兴趣,监听器可以继承 GestureDetector.SimpleOnGestureListener 。在onDown方法中需要返回true,否则后续事件会被忽略。</p>    <h2><strong>手势运动</strong></h2>    <p>手势可以分为运动型和非运动型。比如tap(轻敲)就没有移动,而scroll要求手指有一定的移动距离。手指是否发生运动的判定有一个临界值:touch slop,可以通过 android.view.ViewConfiguration#getScaledTouchSlop 获得,表示触摸被判定为滑动的最小距离。</p>    <p>非运动型手势,比如点击类型的,识别的逻辑主要是对“时间间隙”的检测。运动型手势稍复杂些,对运动的判定根据实际功能需要可以获得有关运动的不同方面:</p>    <ul>     <li>pointer的start和end位置。</li>     <li>根据触摸的x,y坐标计算出的移动方向。</li>     <li>通过 getHistorical</li>     <li>pointer移动时的速度。</li>    </ul>    <h2><strong>VelocityTracker</strong></h2>    <p>有时对手势运动过程中的速度感兴趣,可以通过 android.view.VelocityTracker 来根据收集的事件数据计算得到运动时的速度:</p>    <pre>  <code class="language-java">public class MainActivity extends Activity {    private static final String DEBUG_TAG = "Velocity";        ...    private VelocityTracker mVelocityTracker = null;    @Override    public boolean onTouchEvent(MotionEvent event) {        int index = event.getActionIndex();        int action = event.getActionMasked();        int pointerId = event.getPointerId(index);          switch(action) {            case MotionEvent.ACTION_DOWN:                if(mVelocityTracker == null) {                    // Retrieve a new VelocityTracker object to watch the velocity of a motion.                    mVelocityTracker = VelocityTracker.obtain();                }                else {                    // Reset the velocity tracker back to its initial state.                    mVelocityTracker.clear();                }                // Add a user's movement to the tracker.                mVelocityTracker.addMovement(event);                break;            case MotionEvent.ACTION_MOVE:                mVelocityTracker.addMovement(event);                // When you want to determine the velocity, call                // computeCurrentVelocity(). Then call getXVelocity()                // and getYVelocity() to retrieve the velocity for each pointer ID.                mVelocityTracker.computeCurrentVelocity(1000);                // Log velocity of pixels per second                // Best practice to use VelocityTrackerCompat where possible.                Log.d("", "X velocity: " +                        VelocityTrackerCompat.getXVelocity(mVelocityTracker,                        pointerId));                Log.d("", "Y velocity: " +                        VelocityTrackerCompat.getYVelocity(mVelocityTracker,                        pointerId));                break;            case MotionEvent.ACTION_UP:            case MotionEvent.ACTION_CANCEL:                // Return a VelocityTracker object back to be re-used by others.                mVelocityTracker.recycle();                break;        }        return true;    }  }</code></pre>    <h2><strong>Scroller</strong></h2>    <p>不严谨的区分下,scroll可以分跟随手指的滑动——drag,和手指划过屏幕后的附加减速滑动——fling。</p>    <p>通常,需要对手势运动进行响应,比如画面跟随手指的移动而移动(平移),简单的实现就是在ACTION_MOVE中即时偏移对应的x,y,这种情况下对动作的“响应时机”是显而易见的。另一些情况下,需要达到平滑的滑动效果,但每次执行滑动的时机和滑动的增量都需要计算。比如,点击上一页,下一页按钮后执行的滚动翻页效果——类似ViewPager的动画效果那样。再一种情况是,手指快速划过屏幕后,需要让显示的内容继续滑动然后渐渐停止——fling效果。这些情况下,都需要在未来一段时间内,不断调整画面,达到滚动动画效果——每次执行滑动的时机和偏移量都需要计算。可以借助Scroller来完成“smoothly move”这样的动画效果。</p>    <p>推荐使用 android.widget.OverScroller ,它兼容性好,且支持边缘效果。和VelocityTracker一样,Scroller是一个“计算工具”,它支持startScroll、fling两个滑动效果,和上面的例子对应。从设计上,它独立于滚动效果的执行,只提供对滚动动画过程的计算和状态判定。</p>    <p>Scroller的使用流程:</p>    <ol>     <li>准备Scroller对象。</li>    </ol>    <pre>  <code class="language-java">// 在构造函数,onCreate等合适的初始化的地方  mScroller = new OverScroller(context);</code></pre>    <ol>     <li> <p>在合适的时候开启滚动动画。一般的,fling效果会结合GestureDetector,识别出手指的fling手势后开启滚动动画:在OnGestureListener中的onFling中执行 Scroller.fling() 方法。</p> <p>而 Scroller.fling() 所开启的“平滑的滑动效果”可以在任何需要开启滑动的时候执行。</p> </li>    </ol>    <pre>  <code class="language-java">mScroller.fling(startX, startY, velocityX, velocityY,              minX, maxX, minY, maxY, overX, overY);    mScroller.startScroll(startX, startY, dx, dy, duration);</code></pre>    <ol>     <li>在动画的每一帧的执行时刻,计算滚动增量,应用到具体View对象。在自定义View时,可以依靠 android.view.View#postOnAnimation , android.view.View#postInvalidateOnAnimation() 方法简单的触发在下一动画帧,以执行动画操作。或者使用Animation等可以获得动画帧执行频率的机制。View本身有 computeScroll() 方法可以供子类执行动画式滚动逻辑——结合 postInvalidateOnAnimation() 。</li>    </ol>    <pre>  <code class="language-java">boolean animEnd = false;  if (mScroller.computeScrollOffset()) {    int currX = mScroller.getCurrX();    int currY = mScroller.getCurrY();    // 修改Viewx,y位置,可以使用View的scroll方法  } else {    animEnd = false;  }    if (!animEnd) {    postInvalidateOnAnimation();  }</code></pre>    <p>像 ScrollView , HorizontalScrollView 自身提供了滚动功能,ViewPager也使用Scroller完成平滑的滑动行为。一般在自定义带滑动行为的控件时使用Scroller。框架的几个控件使用 EdgeEffect 完成一些边缘效果。</p>    <h2><strong>Multi-Touch</strong></h2>    <p>上面对MotionEvent的介绍中可以看到,每个处于触摸的手指被当做一个pointer。目前大多数手机设备几乎都是支持10点触摸。</p>    <p>是否考虑多点触摸是根据View的功能而定。比如scroll一般一个手指就可以,而scale这一的就必须2个手指以上了。</p>    <p>MotionEvent的getPointerId和findPointerIndex方法提供了对当前事件数据的每个pointer的标识,根据pointerIndex可以调用其它以它为参数的方法获得对应pointer的不同方面的值。pointerId可以作为一个pointer触屏期间的唯一标识。</p>    <pre>  <code class="language-java">private int mActivePointerId;    public boolean onTouchEvent(MotionEvent event) {      ....      // Get the pointer ID      mActivePointerId = event.getPointerId(0);        // ... Many touch events later...        // Use the pointer ID to find the index of the active pointer      // and fetch its position      int pointerIndex = event.findPointerIndex(mActivePointerId);      // Get the pointer's current position      float x = event.getX(pointerIndex);      float y = event.getY(pointerIndex);  }</code></pre>    <p>对于单点触摸,通常在onTouchEvent方法中根据getAction就可以判定出对应动作。而多点触摸时需要使用getActionMasked方法。区别前面提到了,下面的代码片段给出了有关多点触摸的一般API:</p>    <pre>  <code class="language-java">int action = MotionEventCompat.getActionMasked(event);  // Get the index of the pointer associated with the action.  int index = MotionEventCompat.getActionIndex(event);  int xPos = -1;  int yPos = -1;    Log.d(DEBUG_TAG,"The action is " + actionToString(action));    if (event.getPointerCount() > 1) {      Log.d(DEBUG_TAG,"Multitouch event");      // The coordinates of the current screen contact, relative to      // the responding View or Activity.        xPos = (int)MotionEventCompat.getX(event, index);      yPos = (int)MotionEventCompat.getY(event, index);    } else {      // Single touch event      Log.d(DEBUG_TAG,"Single touch event");      xPos = (int)MotionEventCompat.getX(event, index);      yPos = (int)MotionEventCompat.getY(event, index);  }  ...    // Given an action int, returns a string description  public static String actionToString(int action) {      switch (action) {          case MotionEvent.ACTION_DOWN: return "Down";          case MotionEvent.ACTION_MOVE: return "Move";          case MotionEvent.ACTION_POINTER_DOWN: return "Pointer Down";          case MotionEvent.ACTION_UP: return "Up";          case MotionEvent.ACTION_POINTER_UP: return "Pointer Up";          case MotionEvent.ACTION_OUTSIDE: return "Outside";          case MotionEvent.ACTION_CANCEL: return "Cancel";      }      return "";  }</code></pre>    <p>类MotionEventCompat提供了一些多点触摸相关辅助方法,兼容版本。</p>    <h2><strong>ViewConfiguration</strong></h2>    <p>该类提供了一些UI相关的常量,关于超时时间,大小,和距离等。会根据系统的版本和运行的设备环境,如分辨率,尺寸等,提供统一的标准参考值,为UI元素提供一致的交互体验。</p>    <ul>     <li> <p>Touch Slop</p> <p>表示pointer被视为滚动手势的最小的移动距离。</p> </li>     <li> <p>Fling Velocity</p> <p>表示手指移动被视为触发fling的临界速度。</p> </li>    </ul>    <h2><strong>ViewGroup管理TouchEvent</strong></h2>    <h2><strong>事件拦截</strong></h2>    <p>在非ViewGroup的View中响应触摸事件的“职责”比较单一,就是根据当前View的交互需求识别然后执行交互逻辑。也就是只需要在 android.view.View#onTouchEvent 中处理触摸产生的事件序列。</p>    <p>ViewGroup继承View,所以它本身可以很据需要在 onTouchEvent() 中处理事件。另一方面,作为其它View的parent,它必须对childViews执行layout,并且有控制MotionEvent传递给目标childView的方法 onInterceptTouchEvent() 。注意ViewGroup本身可以处理事件,因为它同时也是合格的View子类。根据类的功能而不同,比如ViewPager会处理左右滑动的事件,但将上下滑动的事件传递给childView。要知到,ViewGroup可以包含View,也可以不包含。所以实际的事件有的是childView应该处理的,有的是“落在”ViewGroup本身区域内。</p>    <h2><strong>相关方法</strong></h2>    <p>有关事件分发的机制这里只简单提及,ViewGroup可以管理MotionEvent的传递。涉及到下面的方法:</p>    <h3><strong>boolean onInterceptTouchEvent(MotionEvent ev)</strong></h3>    <p>该方法用来拦截传递给目标childView(可以是ViewGroup,这里不一定是事件的最终目标view,而是事件传递路径经过当前ViewGroup后的下一个view)的MotionEvent事件,可以做些额外操作,甚至是阻止事件的传递自己处理。如果ViewGroup希望自己的onTouchEvent()处理手势事件,可以重写此方法并在onTouchEvent()中配合完成期望的手势处理。</p>    <ul>     <li>事件经过ViewGroup的顺序</li>    </ul>    <ol>     <li>onInterceptTouchEvent()中接收到down事件,作为后续事件的起点。</li>     <li>down事件可以被childView处理,或者由当前ViewGroup的onTouchEvent()方法处理。自己处理时onInterceptTouchEvent()返回true,对应onTouchEvent()也应该返回true,这样ViewGroup就可以收到后续的事件,否则——onInterceptTouchEvent()返回true,而onTouchEvent()返回false——后续事件将交给ViewGroup的parent处理。在两个方法都返回true之后,后续事件就直接交给ViewGroup的onTouchEvent()去处理,onInterceptTouchEvent()不再收到后续事件。</li>     <li>该方法在donw事件返回false,后续所有事件,先传递到该方法,然后是给对应目标childView:的onTouchEvent()或onInterceptTouchEvent()方法——和当前ViewGroup同样的事件消耗规则。</li>     <li>方法返回true后,目标view收到同样的事件作为最后的事件,动作变为CANCEL,后续事件由ViewGroup的onTouchEvent()处理,该方法也不再收到。</li>    </ol>    <ul>     <li> <p>返回值</p> <p>Return true to steal motion events from the children and have them dispatched to this ViewGroup through onTouchEvent(). The current target will receive an ACTION_CANCEL event, and no further messages will be delivered here.</p> </li>    </ul>    <p>注意:ViewGroup中onInterceptTouchEvent()和onTouchEvent()的合作对事件传递的影响主要体现在down事件的处理上,后续事件的传递受此影响。</p>    <h3><strong>boolean onTouchEvent(MotionEvent event)</strong></h3>    <p>ViewGroup继承View的onTouchEvent(),没有任何改变。</p>    <p>其返回值含义如下:</p>    <p>true表示事件被处理(消耗),这样以后事件的传递终止。</p>    <p>false表示未处理,那么会沿着事件传递的路径依次返回parent中去处理——parent的onTouchEvent()被执行,直到某个parent的onTouchEvent()返回true。</p>    <h3><strong>void requestDisallowInterceptTouchEvent(boolean disallowIntercept)</strong></h3>    <p>该方法上由childView调用的。childView调用后并传递true时,会沿着ViewTree中root到目标View的view hierarchy一直向上依次通知各个parent去设置一个和触摸相关的标记FLAG_DISALLOW_INTERCEPT,传递false或者一次触摸操作结束后会清除此标记。</p>    <p>档ViewGroup包含此标记时,其默认的行为是在通过方法 boolean dispatchTouchEvent(MotionEvent ev) 分发事件的时候会忽略调用 onInterceptTouchEvent() 去拦截事件。</p>    <h2><strong>拓展:Dragging和Scaling</strong></h2>    <h2><strong>Drag操作</strong></h2>    <p>android 3.0以上提供了api对拖拽进行支持,见 View.OnDragListener。下面自己处理onTouchEvent()方法来响应drag操作,移动目标View。</p>    <p>实现的重点是对移动距离的检测,按照设计,从第一个手指触摸目标View引发down操作开始,只要还有手指处于触摸状态,就检测对应手指的移动来移动View。移动的距离是计算pointer的MOVE动作对应事件x,y坐标的距离。需要注意的是,必须是检测同一个pointer,因为允许多点触摸,那么就需要记录一个作为移动参考的pointer——定义为activePointer。规则是:第一个手指ACTION_DOWN时记录对应pointerId作为activePointer,如果有手指离开就记录剩余的某个pointer作为新的activePointer。</p>    <p>在ACTION_MOVE中获得新的x,y和最后的(每次设置activePointer时记录对应x,y作为最后的坐标)坐标进行对比,计算产生的距离就是移动距离。</p>    <pre>  <code class="language-java">// The ‘active pointer’ is the one currently moving our object.  private int mActivePointerId = INVALID_POINTER_ID;    @Override  public boolean onTouchEvent(MotionEvent ev) {      // Let the ScaleGestureDetector inspect all events.      mScaleDetector.onTouchEvent(ev);        final int action = MotionEventCompat.getActionMasked(ev);        switch (action) {      case MotionEvent.ACTION_DOWN: {          final int pointerIndex = MotionEventCompat.getActionIndex(ev);          final float x = MotionEventCompat.getX(ev, pointerIndex);          final float y = MotionEventCompat.getY(ev, pointerIndex);            // Remember where we started (for dragging)          mLastTouchX = x;          mLastTouchY = y;          // Save the ID of this pointer (for dragging)          mActivePointerId = MotionEventCompat.getPointerId(ev, 0);          break;      }        case MotionEvent.ACTION_MOVE: {          // Find the index of the active pointer and fetch its position          final int pointerIndex =                  MotionEventCompat.findPointerIndex(ev, mActivePointerId);              final float x = MotionEventCompat.getX(ev, pointerIndex);          final float y = MotionEventCompat.getY(ev, pointerIndex);            // Calculate the distance moved          final float dx = x - mLastTouchX;          final float dy = y - mLastTouchY;            mPosX += dx;          mPosY += dy;            invalidate();            // Remember this touch position for the next move event          mLastTouchX = x;          mLastTouchY = y;            break;      }        case MotionEvent.ACTION_UP: {          mActivePointerId = INVALID_POINTER_ID;          break;      }        case MotionEvent.ACTION_CANCEL: {          mActivePointerId = INVALID_POINTER_ID;          break;      }        case MotionEvent.ACTION_POINTER_UP: {            final int pointerIndex = MotionEventCompat.getActionIndex(ev);          final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);            if (pointerId == mActivePointerId) {              // This was our active pointer going up. Choose a new              // active pointer and adjust accordingly.              final int newPointerIndex = pointerIndex == 0 ? 1 : 0;              mLastTouchX = MotionEventCompat.getX(ev, newPointerIndex);              mLastTouchY = MotionEventCompat.getY(ev, newPointerIndex);              mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);          }          break;      }      }             return true;  }</code></pre>    <p>上面的方法分别在ACTION_DOWN和ACTION_POINTER_UP中设置mActivePointerId,以及上一次的触摸位置。在ACTION_MOVE中记录移动到的位置,以及更新最后的触摸位置。最后,在UP、CANCEL中清除记录的pointerId。</p>    <p>可见,drag手势的识别重点就是记录作为移动参考的pointerId,它必须是连续的。</p>    <p>对于drag操作的识别和响应,可以直接使用GestureDetector响应其中的 onScroll() 方法即可。</p>    <p>scroll,drag和pan这些都是一样的手势/操作。</p>    <h2><strong>Scale</strong></h2>    <p>可以使用ScaleGestureDetector来检测缩放动作。下面的例子是drag和scale一起识别的代码范例,注意其识别操作对事件的消耗顺序:</p>    <pre>  <code class="language-java">private ScaleGestureDetector mScaleDetector;  private GestureDetector mGestureDetector;  private float mScaleFactor = 1.f;    public MyCustomView(Context mContext){      ...      mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());  }    ...  public boolean onTouchEvent(MotionEvent event) {      boolean retVal = mScaleGestureDetector.onTouchEvent(event);      retVal = mGestureDetector.onTouchEvent(event) || retVal;      return retVal || super.onTouchEvent(event);  }    private class ScaleListener          extends ScaleGestureDetector.SimpleOnScaleGestureListener {      @Override      public boolean onScale(ScaleGestureDetector detector) {          mScaleFactor *= detector.getScaleFactor();            // 控制缩放的最大值          mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));            // 缩放系数变化后通知View重绘          invalidate();          return true;      }  }</code></pre>    <p>关于GestureDetector的用法前面给出了,上面代码片段只展示ScaleGestureDetector、ScaleGestureDetector.SimpleOnScaleGestureListener的一般用法。</p>    <p>注意onTouchEvent()中先执行ScaleGestureDetector的事件检测,然后是GestureDetector的,只要两次识别都未处理时,才调用父类的默认行为。</p>    <h2><strong>小结</strong></h2>    <p>理解手势识别的整体过程是在onTouchEvent中根据MotionEvent事件序列来匹配不同的模式是整片文章的目标。要知道,GestureDetector和ScaleGestureDetector这些框架提供的类型都是方便大家在自定义View时的手势识别功能的实现。只要掌握手势识别的思路,可以自己识别任何期望的触摸事件模式。不过,研究框架GestureDetector的源码,以及一些开源的控件中对手势操作的处理是一个很好的开始。</p>    <h2><strong>资料</strong></h2>    <h2><strong>官方文档</strong></h2>    <p>文章主要内容参考来自api 22的开发文档。</p>    <ul>     <li>Using Touch Gestures<br> 文件路径:/docs/training/gestures/detector.html</li>     <li>Input Events<br> 文件路径:/docs/guide/topics/ui/ui-events.htm</li>    </ul>    <h2><strong>案例:PhotoView</strong></h2>    <p>在自定义View时根据需要会出现监听特殊的手势的需要,这个时候就需要定义自己的GestureDetector类型了。研究系统的GestureDetector类的实现非常有帮助,如果需要识别多种手势时,根据实际的特征,可以设计多个Detector类型,用来识别不同手势,但需要注意在使用它们时对事件的消耗顺序,比如drag和scale手势的先后识别。</p>    <p>开源项目PhotoView用来展示图片并支持各种手势对图片进行缩放,平移等操作。它里面包含了几个手势识别的类,建议可以阅读它的代码来作为对手势识别的“实现细节”的实践。</p>    <p> </p>    <p> </p>    <p>来自:http://www.cnblogs.com/everhad/p/6075716.html</p>    <p> </p>