欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-03-01 17:06:34
摘要 本文是手势识别输入事件处理的完整学习记录。内容包括输入事件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 < 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));
  }
}

前面提到了,事件具有动作分类,而且每个事件对象中包含所有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 < pointercount; p++) {
  system.out.printf(" pointer %d: (%f,%f)",
    ev.getpointerid(p), ev.getx(p), ev.gety(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用来展示图片并支持各种手势对图片进行缩放,平移等操作。它里面包含了几个手势识别的类,建议可以阅读它的代码来作为对手势识别的“实现细节”的实践。

源码下载:项目下载

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