当前位置: 移动技术网 > 移动技术>移动开发>Android > 札记:android手势识别功能实现(利用MotionEvent)

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

2019年07月24日  | 移动技术网移动技术  | 我要评论

摘要

本文是手势识别输入事件处理的完整学习记录。内容包括输入事件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用来展示图片并支持各种手势对图片进行缩放,平移等操作。它里面包含了几个手势识别的类,建议可以阅读它的代码来作为对手势识别的“实现细节”的实践。

源码下载:

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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网