札记:android手势识别功能实现(利用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来响应这些手势。
手势识别过程
为了实现对手势的响应处理,需要理解触摸事件的表示。而识别手势的具体过程包括:
- 获得触摸事件数据。
- 分析是否匹配所支持的某个手势。
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的顺序
- onintercepttouchevent()中接收到down事件,作为后续事件的起点。
- down事件可以被childview处理,或者由当前viewgroup的ontouchevent()方法处理。自己处理时onintercepttouchevent()返回true,对应ontouchevent()也应该返回true,这样viewgroup就可以收到后续的事件,否则——onintercepttouchevent()返回true,而ontouchevent()返回false——后续事件将交给viewgroup的parent处理。在两个方法都返回true之后,后续事件就直接交给viewgroup的ontouchevent()去处理,onintercepttouchevent()不再收到后续事件。
- 该方法在donw事件返回false,后续所有事件,先传递到该方法,然后是给对应目标childview:的ontouchevent()或onintercepttouchevent()方法——和当前viewgroup同样的事件消耗规则。
- 方法返回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用来展示图片并支持各种手势对图片进行缩放,平移等操作。它里面包含了几个手势识别的类,建议可以阅读它的代码来作为对手势识别的“实现细节”的实践。
源码下载:项目下载
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。