热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

札记:android手势识别功能实现(利用MotionEvent)

摘要 本文是手势识别输入事件处理的完整学习记录。内容包括输入事件InputEvent响应方式,触摸事件MotionEvent的概念和使

摘要

本文是手势识别输入事件处理的完整学习记录。内容包括输入事件InputEvent响应方式,触摸事件MotionEvent的概念和使用,触摸事件的动作分类、多点触摸。根据案例和API分析了触摸手势Touch Gesture的识别处理的一般过程。介绍了相关的GestureDetector,Scroller和VelocityTracker。最后分析drag和scale等一些手势的识别。

输入源分类

虽然android本身是一个完整的系统,它主要运行在移动设备的特性决定了我们在它上面开的app绝大数属于客户端程序,主要目标就是显示界面处理交互,这点和web前端以及桌面上的应用类似。

作为“客户端程序”,编写的大部分功能就是处理用户交互。不同系统(对应不同设备)可支持的用户交互各有不同。
android可以运行在多种设备,从交互输入上看, InputDevice.SOURCE_CLASS_xxx常量标识了sdk所支持的几种不同输入源的设备。有:触屏,物理/虚拟按键,摇杆,鼠标等,下面的讨论针对最广泛的交互——触屏( SOURCE_TOUCHSCREEN)。
触屏设备从交互设计上看就是各种手势,有点击,双击,滑动,拖拽,缩放等等交互定义,本质上它们都是基础的几种触摸事件的不同模式的组合。

在安卓触屏系统中,支持单点、多点(点通常就是手指)触摸,每个点有按下,移动和抬起。

触屏交互的处理分不同触屏操作——手势的识别,然后是根据业务对应不同处理。为了响应不同的手势,首先就需要识别它们。识别过程就是跟踪收集系实时提供的反应用户在屏幕上的动作的"基本事件",然后根据这些数据(事件集合)来判定出各种不同种类的高级别的“动作”。

android.view.GestureDetector提供了对onScroll、onLongPress、onFling等几个最常见动作的监听。而自己的app根据需要可以通过实现自己的GestureDetector类型来识别出类似Drag、Scale这样的交互动作。

手势识别是智能手机和平板等触屏设备的主流交互/输入方式,不同于PC上的键盘和鼠标。

输入事件

用户交互产生的输入事件最终由InputEvent的子类来表示,目前包括KeyEvent(Object used to report key and button events)和MotionEvent(Object used to report movement (mouse, pen, finger, trackball) events.)。

接收InputEvent的地方有很多,根据框架对事件的传播路径依次有Activity、Window、View(ViewTree的一条路径:view stack)。

多数情况下都是在用户交互的具体View中接收并处理这些输入事件。

View的事件处理有2种方式,一种是添加监听器(event listener),另一种是重写处理器方法( event handler)。前者比较方便,后者在自定义View时根据需要去重写,而且CustomView也可以根据需要定义自己的处理器方法,或提供监听接口。

事件监听

事件监听接口都是只包含一个方法的interface,如:

// 在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);
}

在Activity等地方通过创建匿名类或实现对应接口(省去新类型和对象的分配)然后调用View.setOn...Listener()来完成注册监听。

根据android的ui-events(输入事件)的传递机制,监听器的回调方法会先于各种相应的处理器方法被执行,对于那些有返回boolean值的回调方法,返回值表示是否让事件继续被传播,所以应该根据需要谨慎设计返回值,否则会阻塞其它处理的执行。

例如,当为View设置OnTouchListener之后,若回调方法onTouch返回true,那么在View的 boolean dispatchTouchEvent(MotionEvent event)中执行了回调方法后,就不再执行View中的处理器方法 boolean onTouchEvent(MotionEvent event)

事件处理器

事件处理器就是在“事件传递”经过当前View时调用的默认方法。通常也就是对应具体View的行为逻辑的实现(要知道监听器不是必须的,甚至可以不去定义,而任何View都会为感兴趣的事件提供处理)。

有关消息传递的知识可以写一整篇了,这里略过,只需要知道,输入事件会沿着ViewTree自顶向下穿过许多“相关的”View,然后这些View处理或继续传递事件。事件到达ViewTree之前还会经过Activity和Window,最终的起源当然是系统负责收集的硬件事件,从“事件管理器”发送给交互中的界面相关的某个类,开始传播。

View类中包括下面的事件处理方法:

  • onKeyDown(int, KeyEvent)- Called when a new key event occurs.
  • onKeyUp(int, KeyEvent) - Called when a key up event occurs.
  • onTrackballEvent(MotionEvent) - Called when a trackball motion event occurs.
  • onTouchEvent(MotionEvent) - Called when a touch screen motion event occurs.
  • onFocusChanged(boolean, int, Rect) - Called when the view gains or loses focus.

上面的处理器方法是站在事件传播管道的当前节点来进行处理的,也就是处理只需要考虑当前View所提供的功能逻辑,并告知调用者是否已经处理结束——需要继续传递?而对于ViewGroup类,它还承担传递事件给childView的任务,下面的方法和事件传递密切相关:

  • Activity.dispatchTouchEvent(MotionEvent) - This allows your Activity to intercept all touch events before they are dispatched to the window.
  • ViewGroup.onInterceptTouchEvent(MotionEvent) - This allows a ViewGroup to watch events as they are dispatched to child Views.
  • ViewParent.requestDisallowInterceptTouchEvent(boolean) - Call this upon a parent View to indicate that it should not intercept touch events with onInterceptTouchEvent(MotionEvent).

了解在哪些地方可以接收事件,什么时候去处理消耗事件是界面编程的一个重要方面,但“输入事件的传递过程”是一个重要且够复杂的话题,本篇文章重点是触屏事件的各种手势识别,相关的知识仅从“理解的完整和条理性”出发占据一定篇幅。

TouchMode

对于触屏设备,用户开始触摸直到离开屏幕(press->lift)期间,界面会处于TouchMode的交互状态。大致来看,所有的View都在响应触摸事件或者其它的KeyEvent(按键,按钮等)事件。两者在交互上截然不同,触摸模式的状态维护贯穿了整个系统,包括所有的Window和Activity对象(主要就是触摸事件的分发的控制),通过View类的 public boolean isInTouchMode ()方法可以查看当前设备是否处在触摸模式。

Gestures

用户手指(一或多个)按下和最终完全离开屏幕的过程为一次触屏操作,每次操作都可归类为不同触摸模式(touch pattern),最终被定义为不同的手势(手势和模式的定义是设计上的,用户在使用任何触屏设备后都会学习到不同的手势),android支持的主要手势有:

  • Touch
  • Long press
  • Swipe or drag
  • Long press drag
  • Double touch
  • Double touch drag
  • Pinch open
  • Pinch close

app需要根据系统提供的API来响应这些手势。

手势识别过程

为了实现对手势的响应处理,需要理解触摸事件的表示。而识别手势的具体过程包括:

  1. 获得触摸事件数据。
  2. 分析是否匹配所支持的某个手势。

MotionEvent

触摸动作触发的输入事件由MotionEvent表示,它实现了Parcelable接口——IPC需求。
目前的设备几乎都支持多点触摸,每个触摸中的手指被当做一个poiner。MotionEvent记录了目前所有处于触摸的poiner,包含它们各自的X,Y坐标,压力,接触区域等信息。

每个手指的按下、移动和抬起都会产生一个事件对象。每个事件对应一个“动作”,由MotionEvent.ACTION_xxx的常量来表示:

  • 在第一个手指按下时,触发ACTION_DOWN
  • 后续手指按下时触发ACTION_POINTER_DOWN
  • 任何一个手指的移动触发ACTION_MOVE
  • 非最后一个手指抬起触发ACTION_POINTER_UP
  • 最后离开屏幕时触发ACTION_UP
  • 触摸事件序列被中断时触发ACTION_CANCEL,一般是对应View的parent阻止的,比如触摸超出区域时。

每一个手指的down,move和up都会产生事件。出于性能考虑,因为移动过程会产生大量的ACTION_MOVE事件,它们被“批量”发送,也就是一个MotionEvent中将可以包含若干个实际的ACTION_MOVE事件数据,很显然,这些事件都是MOVE动作,而且poiner数量是一样的——任何poiner的加入和去除都引发DOWN、UP事件,这样就不是连续的MOVE事件了。

相比上一个MotionEvent数据,当前MotionEvent的所有数据都是最新的。打包的数据根据时间形成数组,而最新的数据被作为current数据。可以通过 getHistorical系列方法访问“历史事件”的数据。

下面是获得当前MotionEvent中所有事件的各个poiner的坐标的标准形式:

void printSamples(MotionEvent ev) {
  final int historySize = ev.getHistorySize();
  final int pointerCount = ev.getPointerCount();
  for (int h = 0; h 

前面提到了,事件具有动作分类,而且每个事件对象中包含所有pointer的相关数据。获得action的方式是:

action = event.getAction() & MotionEvent.ACTION_MASK;

getAction和getActionMasked

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——更准确些。

获得某个pointer的数据的方式也比较特殊,比如获得各个pointer的X坐标:

final int pointerCount = ev.getPointerCount();
// p就是pointerIndex
for (int p = 0; p 

在一次手势操作过程中,pointer的数量可能发生变化,每一个pointer在DOWN事件的时候就获得一个关联的id,可以作为它的有效标识,直至UP或CANCEL后(pointerCount变化)。

在单个的MotionEvent对象中, getPointerCount()返回了处于触摸的pointer的总数,0~getPointerCount()-1的值就是当前所有pointer的pointerIndex。方法 float getX(int pointerIndex)接收index来获得对应pointer的X坐标值。
类似的,其它接收pointerIndex参数的方法用以获得pointer的其它属性。如果需要关注某个手指的连续动作,比如第一个按下的手指,可以通过方法 int getPointerId(int pointerIndex)获得pointerIndex的id,记录此id,然后在每个MotionEvent数据检查时通过方法 int findPointerIndex(int pointerId)得到id在当前MotionEvent数据中对应的pointerIndex,就可以访问连续事件中指定id的pointer的属性了。

最后,MotionEvent的以下方法是经常用到的:

  • long getEventTime() 获得事件发生的时间。
  • long getDownTime() 获得本次触摸事件序列的第一个——手指按下(ACTION_DOWN)的发生时间。
  • int getAction()int getActionMasked()int getActionIndex()int getPointerCount()int getPointerId(int pointerIndex)float getX()float getX(int pointerIndex)等。

接收事件数据

手势操作产生的一系列MotionEvent对象依次分发出去,传递并经过一些UI相关对象,一般的最终会经过对应的Activity和组成界面的那些和当前触屏相关的View对象——沿着ViewTree从事件所在View向上的各个parent。

在当前界面的Activity中,可以通过重写Activity的 boolean onTouchEvent(MotionEvent event)方法来接收触摸事件,更多时候,因为View是具体实现UI交互的地方,所以在View的 boolean onTouchEvent(MotionEvent event)方法中接收事件。
一次触摸操作会发送一系列事件,所以onTouchEvent会被“很多次”调用。

@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);
  }
}

也可以通过设置监听器来接收触摸事件,这是针对具体的View对象进行的:

myView.setOnTouchListener(new OnTouchListener() {
  public boolean onTouch(View v, MotionEvent event) {
    // ... Respond to touch events    
    return true;
  }
});

需要注意的是,不论识别那种手势操作,ACTION_DOWN动作一定需要返回true,否则按照调用约定,将认为当前处理忽略本次触摸操作的事件序列,后续事件不会收到。

检测手势

在重写的onTouch回调方法中根据收到的事件序列就可以判定出各种手势。例如,一个ACTION_DOWN,紧接着是一系列的ACTION_MOVE,然后是ACTION_UP,这样的序列通常就是scroll/drag手势。总的说来,在实现识别手势的逻辑时,需要“精心设计”代码,往往需要考虑多少偏移才被当做有效滑动,多少时间间隙的down、up才算tap。 android.view.GestureDetector提供了对最常见的手势的识别。下面分别对手势识别的关键相关类型做介绍。

GestureDetector

它的作用就是识别onScroll、onFling onDown(), onLongPress()等操作。将收到的MotionEvent序列传递给GestureDetector,之后它触发对应不同手势的回调方法。
使用过程为:

1.准备GestureDetector对象,提供响应各种手势回调方法的监听器。OnGestureListener就是对不同手势的回调接口,很好理解。

// public GestureDetector(Context context, OnGestureListener listener);
mDetector = new GestureDetector(this, mGestureListener);

在onTouch方法中将收到的事件传递给GestureDetector。

@Override
public boolean onTouchEvent(MotionEvent event) {
  boolean handled = mDetector.onTouchEvent(event);
  return handled || super.onTouchEvent(event);
}

如果只对GestureDetector的个别手势的回调感兴趣,监听器可以继承 GestureDetector.SimpleOnGestureListener。在onDown方法中需要返回true,否则后续事件会被忽略。

手势运动

手势可以分为运动型和非运动型。比如tap(轻敲)就没有移动,而scroll要求手指有一定的移动距离。手指是否发生运动的判定有一个临界值:touch slop,可以通过android.view.ViewConfiguration#getScaledTouchSlop获得,表示触摸被判定为滑动的最小距离。

非运动型手势,比如点击类型的,识别的逻辑主要是对“时间间隙”的检测。运动型手势稍复杂些,对运动的判定根据实际功能需要可以获得有关运动的不同方面:

  • pointer的start和end位置。
  • 根据触摸的x,y坐标计算出的移动方向。
  • 通过 getHistorical
  • pointer移动时的速度。

VelocityTracker

有时对手势运动过程中的速度感兴趣,可以通过android.view.VelocityTracker来根据收集的事件数据计算得到运动时的速度:

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;
 }
}

Scroller

不严谨的区分下,scroll可以分跟随手指的滑动——drag,和手指划过屏幕后的附加减速滑动——fling。

通常,需要对手势运动进行响应,比如画面跟随手指的移动而移动(平移),简单的实现就是在ACTION_MOVE中即时偏移对应的x,y,这种情况下对动作的“响应时机”是显而易见的。另一些情况下,需要达到平滑的滑动效果,但每次执行滑动的时机和滑动的增量都需要计算。比如,点击上一页,下一页按钮后执行的滚动翻页效果——类似ViewPager的动画效果那样。再一种情况是,手指快速划过屏幕后,需要让显示的内容继续滑动然后渐渐停止——fling效果。这些情况下,都需要在未来一段时间内,不断调整画面,达到滚动动画效果——每次执行滑动的时机和偏移量都需要计算。可以借助Scroller来完成“smoothly move”这样的动画效果。

推荐使用android.widget.OverScroller,它兼容性好,且支持边缘效果。和VelocityTracker一样,Scroller是一个“计算工具”,它支持startScroll、fling两个滑动效果,和上面的例子对应。从设计上,它独立于滚动效果的执行,只提供对滚动动画过程的计算和状态判定。

Scroller的使用流程:

准备Scroller对象。

// 在构造函数,onCreate等合适的初始化的地方
mScroller = new OverScroller(context);

在合适的时候开启滚动动画。一般的,fling效果会结合GestureDetector,识别出手指的fling手势后开启滚动动画:在OnGestureListener中的onFling中执行Scroller.fling()方法。

而Scroller.fling()所开启的“平滑的滑动效果”可以在任何需要开启滑动的时候执行。

mScroller.fling(startX, startY, velocityX, velocityY,
      minX, maxX, minY, maxY, overX, overY);

mScroller.startScroll(startX, startY, dx, dy, duration);

在动画的每一帧的执行时刻,计算滚动增量,应用到具体View对象。在自定义View时,可以依靠android.view.View#postOnAnimation,android.view.View#postInvalidateOnAnimation()方法简单的触发在下一动画帧,以执行动画操作。或者使用Animation等可以获得动画帧执行频率的机制。View本身有computeScroll()方法可以供子类执行动画式滚动逻辑——结合postInvalidateOnAnimation()。

boolean animEnd = false;
if (mScroller.computeScrollOffset()) {
 int currX = mScroller.getCurrX();
 int currY = mScroller.getCurrY();
 // 修改Viewx,y位置,可以使用View的scroll方法
} else {
 animEnd = false;
}

if (!animEnd) {
 postInvalidateOnAnimation();
}

像ScrollView,HorizontalScrollView自身提供了滚动功能,ViewPager也使用Scroller完成平滑的滑动行为。一般在自定义带滑动行为的控件时使用Scroller。框架的几个控件使用EdgeEffect完成一些边缘效果。

Multi-Touch

上面对MotionEvent的介绍中可以看到,每个处于触摸的手指被当做一个pointer。目前大多数手机设备几乎都是支持10点触摸。
是否考虑多点触摸是根据View的功能而定。比如scroll一般一个手指就可以,而scale这一的就必须2个手指以上了。

MotionEvent的getPointerId和findPointerIndex方法提供了对当前事件数据的每个pointer的标识,根据pointerIndex可以调用其它以它为参数的方法获得对应pointer的不同方面的值。pointerId可以作为一个pointer触屏期间的唯一标识。

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);
}

对于单点触摸,通常在onTouchEvent方法中根据getAction就可以判定出对应动作。而多点触摸时需要使用getActionMasked方法。区别前面提到了,下面的代码片段给出了有关多点触摸的一般API:

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 "";
}

类MotionEventCompat提供了一些多点触摸相关辅助方法,兼容版本。

ViewConfiguration

该类提供了一些UI相关的常量,关于超时时间,大小,和距离等。会根据系统的版本和运行的设备环境,如分辨率,尺寸等,提供统一的标准参考值,为UI元素提供一致的交互体验。

  • Touch Slop:表示pointer被视为滚动手势的最小的移动距离。
  • Fling Velocity:表示手指移动被视为触发fling的临界速度。

ViewGroup管理TouchEvent

事件拦截

在非ViewGroup的View中响应触摸事件的“职责”比较单一,就是根据当前View的交互需求识别然后执行交互逻辑。也就是只需要在android.view.View#onTouchEvent中处理触摸产生的事件序列。

ViewGroup继承View,所以它本身可以很据需要在onTouchEvent()中处理事件。另一方面,作为其它View的parent,它必须对childViews执行layout,并且有控制MotionEvent传递给目标childView的方法onInterceptTouchEvent()。注意ViewGroup本身可以处理事件,因为它同时也是合格的View子类。根据类的功能而不同,比如ViewPager会处理左右滑动的事件,但将上下滑动的事件传递给childView。要知到,ViewGroup可以包含View,也可以不包含。所以实际的事件有的是childView应该处理的,有的是“落在”ViewGroup本身区域内。

相关方法

有关事件分发的机制这里只简单提及,ViewGroup可以管理MotionEvent的传递。涉及到下面的方法:

boolean onInterceptTouchEvent(MotionEvent ev)

该方法用来拦截传递给目标childView(可以是ViewGroup,这里不一定是事件的最终目标view,而是事件传递路径经过当前ViewGroup后的下一个view)的MotionEvent事件,可以做些额外操作,甚至是阻止事件的传递自己处理。如果ViewGroup希望自己的onTouchEvent()处理手势事件,可以重写此方法并在onTouchEvent()中配合完成期望的手势处理。

(1)、事件经过ViewGroup的顺序

  1. onInterceptTouchEvent()中接收到down事件,作为后续事件的起点。
  2. down事件可以被childView处理,或者由当前ViewGroup的onTouchEvent()方法处理。自己处理时onInterceptTouchEvent()返回true,对应onTouchEvent()也应该返回true,这样ViewGroup就可以收到后续的事件,否则——onInterceptTouchEvent()返回true,而onTouchEvent()返回false——后续事件将交给ViewGroup的parent处理。在两个方法都返回true之后,后续事件就直接交给ViewGroup的onTouchEvent()去处理,onInterceptTouchEvent()不再收到后续事件。
  3. 该方法在donw事件返回false,后续所有事件,先传递到该方法,然后是给对应目标childView:的onTouchEvent()或onInterceptTouchEvent()方法——和当前ViewGroup同样的事件消耗规则。
  4. 方法返回true后,目标view收到同样的事件作为最后的事件,动作变为CANCEL,后续事件由ViewGroup的onTouchEvent()处理,该方法也不再收到。

(2)、返回值

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.

注意:ViewGroup中onInterceptTouchEvent()和onTouchEvent()的合作对事件传递的影响主要体现在down事件的处理上,后续事件的传递受此影响。

boolean onTouchEvent(MotionEvent event)

ViewGroup继承View的onTouchEvent(),没有任何改变。

其返回值含义如下:

true表示事件被处理(消耗),这样以后事件的传递终止。

false表示未处理,那么会沿着事件传递的路径依次返回parent中去处理——parent的onTouchEvent()被执行,直到某个parent的onTouchEvent()返回true。

void requestDisallowInterceptTouchEvent(boolean disallowIntercept)

该方法上由childView调用的。childView调用后并传递true时,会沿着ViewTree中root到目标View的view hierarchy一直向上依次通知各个parent去设置一个和触摸相关的标记FLAG_DISALLOW_INTERCEPT,传递false或者一次触摸操作结束后会清除此标记。

档ViewGroup包含此标记时,其默认的行为是在通过方法boolean dispatchTouchEvent(MotionEvent ev)分发事件的时候会忽略调用onInterceptTouchEvent()去拦截事件。

拓展:Dragging和Scaling

Drag操作

android 3.0以上提供了api对拖拽进行支持,见 View.OnDragListener。下面自己处理onTouchEvent()方法来响应drag操作,移动目标View。

实现的重点是对移动距离的检测,按照设计,从第一个手指触摸目标View引发down操作开始,只要还有手指处于触摸状态,就检测对应手指的移动来移动View。移动的距离是计算pointer的MOVE动作对应事件x,y坐标的距离。需要注意的是,必须是检测同一个pointer,因为允许多点触摸,那么就需要记录一个作为移动参考的pointer——定义为activePointer。规则是:第一个手指ACTION_DOWN时记录对应pointerId作为activePointer,如果有手指离开就记录剩余的某个pointer作为新的activePointer。

在ACTION_MOVE中获得新的x,y和最后的(每次设置activePointer时记录对应x,y作为最后的坐标)坐标进行对比,计算产生的距离就是移动距离。

// 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;
}

上面的方法分别在ACTION_DOWN和ACTION_POINTER_UP中设置mActivePointerId,以及上一次的触摸位置。在ACTION_MOVE中记录移动到的位置,以及更新最后的触摸位置。最后,在UP、CANCEL中清除记录的pointerId。

可见,drag手势的识别重点就是记录作为移动参考的pointerId,它必须是连续的。

对于drag操作的识别和响应,可以直接使用GestureDetector响应其中的onScroll()方法即可。

scroll,drag和pan这些都是一样的手势/操作。

Scale

可以使用ScaleGestureDetector来检测缩放动作。下面的例子是drag和scale一起识别的代码范例,注意其识别操作对事件的消耗顺序:

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;
  }
}

关于GestureDetector的用法前面给出了,上面代码片段只展示ScaleGestureDetector、ScaleGestureDetector.SimpleOnScaleGestureListener的一般用法。

注意onTouchEvent()中先执行ScaleGestureDetector的事件检测,然后是GestureDetector的,只要两次识别都未处理时,才调用父类的默认行为。

小结

理解手势识别的整体过程是在onTouchEvent中根据MotionEvent事件序列来匹配不同的模式是整片文章的目标。要知道,GestureDetector和ScaleGestureDetector这些框架提供的类型都是方便大家在自定义View时的手势识别功能的实现。只要掌握手势识别的思路,可以自己识别任何期望的触摸事件模式。不过,研究框架GestureDetector的源码,以及一些开源的控件中对手势操作的处理是一个很好的开始。

资料

官方文档

文章主要内容参考来自api 22的开发文档。

Using Touch Gestures

文件路径:/docs/training/gestures/detector.html

Input Events

文件路径:/docs/guide/topics/ui/ui-events.htm

案例:PhotoView

在自定义View时根据需要会出现监听特殊的手势的需要,这个时候就需要定义自己的GestureDetector类型了。研究系统的GestureDetector类的实现非常有帮助,如果需要识别多种手势时,根据实际的特征,可以设计多个Detector类型,用来识别不同手势,但需要注意在使用它们时对事件的消耗顺序,比如drag和scale手势的先后识别。

开源项目PhotoView用来展示图片并支持各种手势对图片进行缩放,平移等操作。它里面包含了几个手势识别的类,建议可以阅读它的代码来作为对手势识别的“实现细节”的实践。

源码下载:项目下载

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


推荐阅读
  • Android 图像色彩处理技术详解
    本文详细探讨了 Android 平台上的图像色彩处理技术,重点介绍了如何通过模仿美图秀秀的交互方式,利用 SeekBar 实现对图片颜色的精细调整。文章展示了具体的布局设计和代码实现,帮助开发者更好地理解和应用图像处理技术。 ... [详细]
  • 新年伊始,正是学习的最佳时机。本文全面解析了CK1957-Zookeeper的核心概念与实践技巧,旨在帮助初学者快速掌握这一深度学习工具。通过详细的理论讲解和实际操作示例,读者可以更好地理解Zookeeper的工作原理及其在分布式系统中的应用。无论是新手还是有一定基础的学习者,都能从中受益匪浅。 ... [详细]
  • Python学习:环境配置与安装指南
    Python作为一种跨平台的编程语言,适用于Windows、Linux和macOS等多种操作系统。为了确保本地已成功安装Python,用户可以通过终端或命令行界面输入`python`或`python3`命令进行验证。此外,建议使用虚拟环境管理工具如`venv`或`conda`,以便更好地隔离不同项目依赖,提高开发效率。 ... [详细]
  • 在 Android 开发中,通过合理利用系统通知服务,可以显著提升应用的用户交互体验。针对 Android 8.0 及以上版本,开发者需首先创建并注册通知渠道。本文将详细介绍如何在应用中实现这一功能,包括初始化通知管理器、创建通知渠道以及发送通知的具体步骤,帮助开发者更好地理解和应用这些技术细节。 ... [详细]
  • 斐波那契数在组合数学中的应用与探索
    斐波那契数列作为数学领域中一个广为人知的数列,不仅拥有丰富的数学性质,还与自然界的诸多现象紧密相连。本文将深入探讨这一数列背后的奥秘,揭示其在组合数学中的广泛应用,并通过具体问题的引入,展示斐波那契数列在解决复杂组合问题时的独特优势。 ... [详细]
  • Jenkins学习精华:自动化构建与持续集成入门指南
    本文综合了网络资源及同事分享的PPT内容,详细介绍了Jenkins在自动化构建与持续集成中的应用。首先涵盖了Jenkins的安装与配置流程,接着阐述了如何根据项目需求设定自动化编译任务,包括确定开发环境、选择合适的编译工具以及实现代码的自动更新等关键步骤。特别强调了在SVN环境中通过命令行实现代码自动拉取的最佳实践。 ... [详细]
  • 当前,众多初创企业对全栈工程师的需求日益增长,但市场中却存在大量所谓的“伪全栈工程师”,尤其是那些仅掌握了Node.js技能的前端开发人员。本文旨在深入探讨全栈工程师在现代技术生态中的真实角色与价值,澄清对这一角色的误解,并强调真正的全栈工程师应具备全面的技术栈和综合解决问题的能力。 ... [详细]
  • 深入解析Gradle中的Project核心组件
    在Gradle构建系统中,`Project` 是一个核心组件,扮演着至关重要的角色。通过使用 `./gradlew projects` 命令,可以清晰地列出当前项目结构中包含的所有子项目,这有助于开发者更好地理解和管理复杂的多模块项目。此外,`Project` 对象还提供了丰富的配置选项和生命周期管理功能,使得构建过程更加灵活高效。 ... [详细]
  • jQuery学习笔记:深入理解事件委派(2014年8月3日)
    在jQuery中,事件委托机制主要通过`closest()`方法实现。该方法用于查找与指定选择器匹配的最近祖先元素,从当前元素开始逐级向上遍历DOM树。这一技术不仅提高了代码的效率,还能有效处理动态生成的元素。参考资料:jQuery遍历方法详解。 ... [详细]
  • 全面解析:Hadoop技术栈中的Linux操作系统概览
    全面解析:Hadoop技术栈中的Linux操作系统概览 ... [详细]
  • 题目描述:小K不幸被LL邪教洗脑,洗脑程度之深使他决定彻底脱离这个邪教。在最终离开前,他计划再进行一次亚瑟王游戏。作为最后一战,他希望这次游戏能够尽善尽美。众所周知,亚瑟王游戏的结果很大程度上取决于运气,但通过合理的策略和算法优化,可以提高获胜的概率。本文将详细解析洛谷P3239 [HNOI2015] 亚瑟王问题,并提供具体的算法实现方法,帮助读者更好地理解和应用相关技术。 ... [详细]
  • 本文详细探讨了 Android 线性布局中的关键属性及其应用技巧,包括 `Gravity`、`Padding` 和 `Weight` 等。具体介绍了 `android:gravity` 属性,用于控制控件内部内容的对齐方式,以及 `android:layout_gravity` 属性,用于确定控件在父布局中的位置。此外,还深入解析了 `android:layout_weight` 的使用方法,帮助开发者更灵活地管理布局空间分配。 ... [详细]
  • 深入探讨Photoshop的高级应用与技巧
    本文深入探讨了Photoshop的高级应用与技巧,不仅涵盖了常用的快捷键,如矩形选框工具(M)、移动工具(V)、套索工具(L)、魔棒工具(W)、裁剪工具(C)等,还介绍了更多专业功能,如图层蒙版、混合模式和智能对象的使用方法,帮助用户提升图像处理的效率和质量。 ... [详细]
  • IDEA中高效利用代码变量名替换功能提升编程效率
    在使用 IntelliJ IDEA 进行公司项目代码审查时,我发现许多变量的命名不符合驼峰式命名规范。起初,我尝试手动逐个修改这些变量名,但效率低下。后来,我偶然发现了 IDEA 中的代码变量名替换功能,这极大地提高了我的工作效率。通过该功能,我可以快速批量地将不规范的变量名修改为符合命名规则的形式,不仅节省了时间,还减少了出错的可能性。此外,我还利用这一功能对整个项目的代码进行了全面的优化,确保所有变量命名一致且易于理解。 ... [详细]
  • JVM参数设置与命令行工具详解
    JVM参数配置与命令行工具的深入解析旨在优化系统性能,通过合理设置JVM参数,确保在高吞吐量的前提下,有效减少垃圾回收(GC)的频率,进而降低系统停顿时间,提升服务的稳定性和响应速度。此外,本文还将详细介绍常用的JVM命令行工具,帮助开发者更好地监控和调优JVM运行状态。 ... [详细]
author-avatar
henkoanna
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有