原文链接 Android View 事件派发流程
自从乔帮主横空出世推出了iPhone以来,触控式的操作便成了21世纪智能设施的规范输出形式。对于同是智能操作系统的Android来说,也不例外。事件,特地是触控事件对于挪动利用程序开发来说是一个十分重要的,也是开发人猿必须把握的事件。这里就要讨论一下Android View中的Event零碎,重点探讨一下事件的派发流程。
输出事件综述
事件的分类
对于Android零碎来说用户输出事件分为两类,一个是KeyEvent,这个是硬件产生的事件,或者更精确的说是非触控手势产生的事件,通常包含硬件按扭如音量键,电源键,零碎导航(HOME,BACK和MENU)以及外设(如外接设备,键盘,自拍杆等)零碎层也都会对立的做成映射转成KeyEvent传给以后Window窗口(以后过程)。
还有一类就是专指解控屏幕产生的触摸式的手势事件,是MotionEvent,为啥不叫TouchEvent呢,因为啊最后的Android版本是反对滑动球的,当初曾经没有这种设施,然而名字就这么流传下来了。这个事件专门由视图零碎view tree来解决,本文也将重点探讨此类事件。
事件从哪里来
简略来说事件是源自于硬件,比方屏幕或者按键,这是废话,晓得了这个意义也不大,硬件产生电子信号后会经由驱动传给内核,内核再报给输出零碎,再传给wms(Windows Manager Server),最终会到Window这里。对于应用层来说,能够了解 为事件都是从Window对象这里来的就行了。
谁先收到事件
对于GUI应用程序层来说,wms就是事件起源,那么ViewRootImpl对象是第一个接管到事件,ViewRootImpl并没有间接把事件派发给view tree,而是先给到DecorView,宿主组件在DecorView处有一个专门接管事件的回调,由此事件便到了以后的宿主组件如Activity或者Dialog,看它是否违心做解决,如果它不解决,那么就会把事件再派发给GUI视图零碎,也即view tree,这一次没有再通过ViewRootImpl对象,而是由Window对象间接调用根节点的dispatchTouchEvent或者dispatchKeyEvent。
15:57:02.254 W/System.err: java.lang.Exception: Stack trace 15:57:02.255 W/System.err: at java.lang.Thread.dumpStack(Thread.java:1348) 15:57:02.256 W/System.err: at net.toughcoder.view.ViewWindowExampleActivity.dispatchKeyEvent(ViewWindowExampleActivity.java:107) 15:57:02.256 W/System.err: at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:342) 15:57:02.256 W/System.err: at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:5053) 15:57:02.257 W/System.err: at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4921) 15:57:02.257 W/System.err: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4442) 15:57:02.258 W/System.err: at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4495) 15:57:02.258 W/System.err: at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4461) 15:57:02.259 W/System.err: at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4601) 15:57:02.259 W/System.err: at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4469) 15:57:02.259 W/System.err: at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4658) 15:57:02.260 W/System.err: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4442) 15:57:02.260 W/System.err: at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4495) 15:57:02.260 W/System.err: at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4461) 15:57:02.261 W/System.err: at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4469) 15:57:02.261 W/System.err: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4442) 15:57:02.261 W/System.err: at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4495) 15:57:02.262 W/System.err: at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4461) 15:57:02.262 W/System.err: at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4634) 15:57:02.263 W/System.err: at android.view.ViewRootImpl$ImeInputStage.onFinishedInputEvent(ViewRootImpl.java:4795) 15:57:02.263 W/System.err: at android.view.inputmethod.InputMethodManager$PendingEvent.run(InputMethodManager.java:2571) 15:57:02.263 W/System.err: at android.view.inputmethod.InputMethodManager.invokeFinishedInputEventCallback(InputMethodManager.java:2081) 15:57:02.264 W/System.err: at android.view.inputmethod.InputMethodManager.finishedInputEvent(InputMethodManager.java:2072) 15:57:02.264 W/System.err: at android.view.inputmethod.InputMethodManager$ImeInputEventSender.onInputEventFinished(InputMethodManager.java:2548) 15:57:02.265 W/System.err: at android.view.InputEventSender.dispatchInputEventFinished(InputEventSender.java:141) 15:57:02.265 W/System.err: at android.os.MessageQueue.nativePollOnce(Native Method) 15:57:02.265 W/System.err: at android.os.MessageQueue.next(MessageQueue.java:326) 15:57:02.265 W/System.err: at android.os.Looper.loop(Looper.java:160) 15:57:02.266 W/System.err: at android.app.ActivityThread.main(ActivityThread.java:6692) 15:57:02.266 W/System.err: at java.lang.reflect.Method.invoke(Native Method) 15:57:02.266 W/System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) 15:57:02.266 W/System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
15:57:14.582 W/System.err: java.lang.Exception: Stack trace 15:57:14.586 W/System.err: at java.lang.Thread.dumpStack(Thread.java:1348) 15:57:14.586 W/System.err: at net.toughcoder.view.DemoEventView.dispatchTouchEvent(DemoEventView.java:24) 15:57:14.586 W/System.err: at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030) 15:57:14.586 W/System.err: at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662) 15:57:14.586 W/System.err: at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030) 15:57:14.587 W/System.err: at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662) 15:57:14.587 W/System.err: at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030) 15:57:14.587 W/System.err: at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662) 15:57:14.587 W/System.err: at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030) 15:57:14.587 W/System.err: at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662) 15:57:14.587 W/System.err: at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:440) 15:57:14.588 W/System.err: at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1830) 15:57:14.588 W/System.err: at android.app.Activity.dispatchTouchEvent(Activity.java:3400) 15:57:14.588 W/System.err: at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:398) 15:57:14.588 W/System.err: at android.view.View.dispatchPointerEvent(View.java:12753) 15:57:14.588 W/System.err: at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5122) 15:57:14.588 W/System.err: at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4925) 15:57:14.588 W/System.err: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4442) 15:57:14.588 W/System.err: at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4495) 15:57:14.589 W/System.err: at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4461) 15:57:14.589 W/System.err: at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4601) 15:57:14.589 W/System.err: at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4469) 15:57:14.589 W/System.err: at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4658) 15:57:14.589 W/System.err: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4442) 15:57:14.589 W/System.err: at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4495) 15:57:14.589 W/System.err: at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4461) 15:57:14.589 W/System.err: at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4469) 15:57:14.590 W/System.err: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4442) 15:57:14.590 W/System.err: at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7117) 15:57:14.590 W/System.err: at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7086) 15:57:14.590 W/System.err: at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7047) 15:57:14.590 W/System.err: at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7220) 15:57:14.590 W/System.err: at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:187) 15:57:14.590 W/System.err: at android.os.MessageQueue.nativePollOnce(Native Method) 15:57:14.590 W/System.err: at android.os.MessageQueue.next(MessageQueue.java:326) 15:57:14.591 W/System.err: at android.os.Looper.loop(Looper.java:160) 15:57:14.591 W/System.err: at android.app.ActivityThread.main(ActivityThread.java:6692) 15:57:14.591 W/System.err: at java.lang.reflect.Method.invoke(Native Method) 15:57:14.591 W/System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) 15:57:14.591 W/System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
因而,从应用程序的角度来说,第一个收到事件的是Activity或者Dialog这种持有Window的顶级组件,所以如果想要从窗口级别来拦挡掉所有的事件,那么Activity会是最好的抉择,代码示例:
@Override public boolean dispatchKeyEvent(KeyEvent event) { if (want to intercept all key events) { return true; } return super.dispatchKeyEvent(event); } @Override public boolean dispatchTouchEvent(MotionEvent event) { if (want to intercept all touch events) { return true; } return super.dispatchTouchEvent(event); }
下面两个办法是在view tree之前最先收到事件的办法,是组件外面想拦挡的最佳地点,这是从后面打先锋。而要想解决掉view tree未解决的事件,则须要在onKeyUp(int keyCode, KeyEvent event))和onKeyDown(int keyCode, KeyEvent event))以及onTouchEvent(MotionEvent event))这几个办法外面解决,这个相当于是断后。
事件的散发过程(Event Propagation)
事件达到应用程序这一端后,从Activity开始了散发过程,它的机制 和过程好比一个各处流动的小球,每个节点都接管一个事件对象,返回一个boolean,如果返回true则示意事件在此被耗费,以后事件散发终止,而如果返回false,示意以后节点对此事件不感兴趣,事件持续散发。
而具体的流程,则是先到Activity(Dialog等第一级组件),再到view tree,在view tree外面也是如此从父view到子view如此一个一个的传递,这个先后顺序流程则是由整个零碎构架决定的。
事件是一个数据流
后面讲的事件的散发过程,就能够看作是一个球在在流动,这是从单个事件的解决角度看,是这样。但从整个的事件来看更如此,因为事件通常像电子信号一样,是从起源登程(如触摸屏,硬件等),有肯定工夫距离的一连串的事件对象的派发的整个过程,简略来比喻就是几个球,每隔1秒就收回一个球,这样一个数据流。
说了这么多,其实实在要做起来还是比较简单的,尽管是一个数据流,然而每 一个流都有开始和完结的标记,解决起来并不难。比方KeyEvent,开始是onKeyDown,而后是onKeyUp,在这两个外面解决,就实现了对KeyEvent流的解决。
而MotionEvent则稍简单一些,一个MotionEvent流,零碎会一直的回调onTouchEvent,直到完结,通过MotionEvent#getAction())来判断,从ACTION_DOWN到ACTION_MOVE到ACTION_UP或者ACTION_CANCEL完结。
留神:因为KeyEvent的解决绝对较简略,所以剩下的局部将重点探讨MotionEevnt。
Touch Event的派发流程
事件产生当前,会传递给Activity#dispatchTouchEvent,如果没有被拦挡,那么就会传给Window,而Window则会传给ViewRootImpl来解决,view tree解决完后,会再交给Activity#onTouchEvent:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
这个办法能够分明地看到,先锋和断后和view tree在事件派发流动中的程序。
上面重点看看在Window中(view tree)外面事件的派发流动过程。其实重点看View#dispatchTouchEvent和ViewGroup#dispatchTouchEvent就能够了,须要留神的是,事件的派发流程与解决流程是不一样的,派发在先,解决在后,所以如果看事件的派发须要看dispatch打头的办法,而解决则是看on打头的。
View的dispatch较为简单一些,因为它提供的是一个默认的实现,并且View是作为view tree中的一个叶子的,因而它的dispatch实际上就是一个起点,所以它做的事件就是,看是否有OnTouchListener,有就调用其onTouch,而后再调用onTouchEvent把事件处理一下,就完了。从这里也能够看进去OnTouchListener是走在onTouchEvent办法的后面的。
至于ViewGroup则绝对简单,因为它要治理子View,向子View派发事件,并且还要解决拦挡。它的逻辑大略是:先看本人是否要拦挡onInterceptTouchEvent)返回true示意要拦挡,false不会拦挡,如果要拦挡,则调用本人的onTouchEvent解决掉事件,而后终止派发(实在的逻辑要稍微简单一些,不同的ACTION解决逻辑不一样)。
重点讲一下ViewGroup是如何向子View派发事件的,当不拦挡的时候,这是比拟惯例的时候,会把事件向子View派发,来捋一捋这一流程:首先,会通过buildTouchDispatchChildList这个办法来抉择子View的程序,这个办法是把子View按事件派发的流程来排序,这个程序是就是用户看到的程序,会以Z轴(屏幕从里到外)来排序,以及渲染(draw)的程序,毕竟从用户角度看最先点击到的,必定 是Z轴最大(离用户最近),最先draw完的(没有被遮挡)。而后按这个程序,按个子View调用其下面的dispatchTouchEvent,就把事件向子View传递了过来,当然 这个也是事件在流动,一旦事件被耗费,就会进行派发。
从这个过程来看view tree事件派发是个深度优先的过程,所以view tree的深度不单单影响渲染的性能,连事件处理也比扁平的要慢一些。
Touch Event事件处理办法
事件的解决也即是各种on结尾的办法如onTouchEvent,或者各种listener(OnClickListener,OnTouchListener)。个别惯例来说设置各种listener就够了,但如果想要自定义一些就间接override onTouchEvent办法,这里就不细说了,各种教程太多了。
listener与间接Override父类办法的区别
须要留神的是如果要override,那么必定 要自定义View才能够,所以这个是更『黑客式』的办法,只有有必要自定义View,且惯例各种listener不能满足需要才有必要如此做,如实现各种自定义的手势等。
listener最大的益处是,很简略不便,隔离性好,事件的触发与后果是隔离的,想针对 事件做解决,实现一个接口就好了,至于事件条件的触发则不必关怀,任何对象都能够实现接口以处理事件,而不用非去子类化(继承)View对象。
还须要留神的是OnTouchListener产生的工夫要早于onTouchEvent,而惯例的手势回调接口(如OnClickListener和onLongClickListner)是在onTouchEvent中触发的。因而,OnTouchListener其实也是一个更为低级的『黑客式』的接口,个别当须要自定义辨认手势时才须要实现此接口。
避免点击穿透
有些时候会有一些点击穿透的问题呈现,比方写了一个布局,外面有几个Button和TextView,然而当点击这些次要内容之外的空白区域时,此页面下一层的Button却收到事件,比方触发了其onClick事件。当应用层叠 式的Fragment时,此问题较常见。其实从View#onTouchEvent中就能够看到解决方案,如此某个View是clickable的,那么它会把事件消耗掉,而如果clickable为false就会持续传递。
呈现穿透的起因就是空白区域,只有这个层页面的一个根布局,通常会是一个ViewGroup,而大部分的ViewGroup默认clickable都是false,因此事件会持续向view tree外面传递,直到其被耗费。
此类问题最简略的解决就是把View设置为clickable="true",这个在布局文件中就能够设置。
根底手势辨认
根底的手势辨认,是说对于触控式操作的一些简略的操作的分类,比方轻触屏幕马上拿开,这视为点击(click或者叫press,或者叫tap),长按屏幕视为long click或者叫long press,还有滑动,双击等等。手势辨认,即是一套逻辑算法,用以判断用户以后是哪一种操作,而后触发相当的解决逻辑,给与用户操作上的反馈。废话就这么多,接下来来看具体如何做吧。
在Android的GUI零碎中根底的手势有点击(click)和长按(long click)。要辨认这些根底手势有两种办法,一是设置接口回调给View,也即实现一个OnClickListener,而后把此对象设置给View#setOnClickListener)(长按就是OnLongClickListener和View#setOnLongClickListener));另外一种办法,就是针对view tree外部,比方子类化(继承)某个View对象,而后override相应的办法。
留神: 在写布局xml文件中也能够不便的用onclick属于来指定 手势回调办法,但它的实质与设置一个OnClickListener是一样的。
如果,点击和长按不能满足操作需要时,就须要稍复简单的根底手势辨认对象来帮忙,也即是GestureDetector,它与View的连贯形式是接口拆散,其实不见得能够用于View,只有有MotionEvent事件 起源即可。应用的办法并不简单,只须要设置一个OnTouchListener或者子类化View并override onTouch办法,从中拿到MotionEvent对象,而后把MotionEvent塞给一个GestureDetecotor对象,就完了,GestureDetector会回调你感兴趣的对应手势解决回调办法,通过OnGestureListener对象。因为OnGestureListener是一个接口,但如果你仅对某几个手势回调办法感兴趣,不想把所有办法都 写一遍(哪怕是空实现),那么能够子类化SimpleOnGestureListener,这是一个类,它实现了OnGestureListener的所有办法,咱们仅须要override感兴趣的办法即可。
有一个须要特地留神的事件就是,当你用GestureDetector时,它与惯例的onClick或者onLongClick的先后顺序,或者 叫抵触解决。基于一致性的准则,如果应用了GestureDetector时,意味着你想要本人管制事件处理,那么就不应该再 设置onClick或者 onLongClick了。但如果真不小心这么做了,后果又会怎么样呢?这就须要从View的事件处理流程找答案。OnTouchListener的调用是在View#dispatchTouchEvent,这个是在View#onTouchEvent之前,而OnClickListener和OnLongClickListener是在View#onTouchEvent中调用的。所以,程序是这样的:
- 如果你用OnTouchListener获取的MotionEvent,那么你的OnGestureListener的回调办法是最先被调用到的,在所有的其余回调之前。
- 如果是override View#onTouchEvent办法获取的event,那么取决于你调用super#onTouchEvent的程序,如果你是在调用super之前,那么还是你的gesture listener先执行。其实吧,正常人override的写法必定都先写本人的逻辑最初再调用super,或者罗唆不调用super,这是最正统子类override父类的姿式。
由此,能够得出的论断就是如果应用了GestureDetector,那么你的gesture listener必定是优先被执行的。
onClick与onLongClick的触发机会
再 来看另外 一个比拟 有意思的两个问题,onClick的触发机会是啥时候?从View#onTouchEvent办法中可看进去,是在ACTION_UP时触发的,如果它还没有触发long click,而long click则是在事件开始当前ACTION_DOWN当前开始计时,达到肯定工夫距离后便触发,不算后续的事件类型。
整体的流程是这样,在View#onTouchEvent外面,分事件类型来解决,ACTION_DOWN中开始计时,前面ACTION_MOVE中持续计时,如果达到长按规范,则触发long click,在失常完结的ACTION_UP中,看有没有达到长按规范,有就触发long click,没有则触发on click。
零碎阈值定义
像长按的时长,滑动的最小间隔,拉伸的最小间隔等 等 这些要害的阈值都 是有零碎倡议的定义的,这些值都 在ViewConfiguration外面,通常倡议间接应用零碎定义的要好一些,除非真有非凡须要。
能够查看GestureDetector中对这些常量的应用。
参考资料
- Mastering the Android Touch System
- How are Android touch events delivered?