Android事件传递机制
实验环境
os x 10.9
eclipse(adt)
android源码版本:api level 19(android 4.4)
android事件构成
在android中,事件主要包括点按、长按、拖拽、滑动等,点按又包括单击和双击,另外还包括单指操作和多指操作。所有这些都构成了android中的事件响应。总的来说,所有的事件都由如下三个部分作为基础:
按下(action_down)
移动(action_move)
抬起(action_up)
所有的操作事件首先必须执行的是按下操作(actiondown),之后所有的操作都是以按下操作作为前提,当按下操作完成后,接下来可能是一段移动(actionmove)然后抬起(action_up),或者是按下操作执行完成后没有移动就直接抬起。这一系列的动作在android中都可以进行控制。
我们知道,所有的事件操作都发生在触摸屏上,而在屏幕上与我们交互的就是各种各样的视图组件(view),在android中,所有的视图都继承于view,另外通过各种布局组件(viewgroup)来对view进行布局,viewgroup也继承于view。所有的ui控件例如button、textview都是继承于view,而所有的布局控件例如relativelayout、容器控件例如listview都是继承于viewgroup。所以,我们的事件操作主要就是发生在view和viewgroup之间,那么view和viewgroup中主要有哪些方法来对这些事件进行响应呢?记住如下3个方法,我们通过查看view和viewgroup的源码可以看到:
view.java
public boolean dispatchtouchevent(motionevent event) public boolean ontouchevent(motionevent event)
viewgroup.java
public boolean dispatchtouchevent(motionevent event) public boolean ontouchevent(motionevent event) public boolean onintercepttouchevent(motionevent event)
在view和viewgroup中都存在dispatchtouchevent和ontouchevent方法,但是在viewgroup中还有一个onintercepttouchevent方法,那这些方法都是干嘛的呢?别急,我们先看看他们的返回值。这些方法的返回值全部都是boolean型,为什么是boolean型呢,看看本文的标题,“事件传递”,传递的过程就是一个接一个,那到了某一个点后是否要继续往下传递呢?你发现了吗,“是否”二字就决定了这些方法应该用boolean来作为返回值。没错,这些方法都返回true或者是false。在android中,所有的事件都是从开始经过传递到完成事件的消费,这些方法的返回值就决定了某一事件是否是继续往下传,还是被拦截了,或是被消费了。
接下来就是这些方法的参数,都接受了一个motionevent类型的参数,motionevent继承于inputevent,用于标记各种动作事件。之前提到的actiondown、actionmove、action_up都是motinevent中定义的常量。我们通过motionevent传进来的事件类型来判断接收的是哪一种类型的事件。到现在,这三个方法的返回值和参数你应该都明白了,接下来就解释一下这三个方法分别在什么时候处理事件。
dispatchtouchevent方法用于事件的分发,android中所有的事件都必须经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。返回true表示不继续分发,事件没有被消费。返回false则继续往下分发,如果是viewgroup则分发给onintercepttouchevent进行判断是否拦截该事件。
ontouchevent方法用于事件的处理,返回true表示消费处理当前事件,返回false则不处理,交给子控件进行继续分发。
onintercepttouchevent是viewgroup中才有的方法,view中没有,它的作用是负责事件的拦截,返回true的时候表示拦截当前事件,不继续往下分发,交给自身的ontouchevent进行处理。返回false则不拦截,继续往下传。这是viewgroup特有的方法,因为viewgroup中可能还有子view,而在android中view中是不能再包含子view的(ios可以)。
到目前为止,android中事件的构成以及事件处理方法的作用你应该比较清楚了,接下来我们就通过一个demo来实际体验实验一下。
android事件处理
首先在eclipse新建一个工程,并新建一个类rtbutton继承button,用来实现我们对按钮事件的跟踪。
rtbutton.java
public class rtbutton extends button { public rtbutton(context context, attributeset attrs) { super(context, attrs); } @override public boolean dispatchtouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: system.out.println("rtbutton---dispatchtouchevent---down"); break; case motionevent.action_move: system.out.println("rtbutton---dispatchtouchevent---move"); break; case motionevent.action_up: system.out.println("rtbutton---dispatchtouchevent---up"); break; default: break; } return super.dispatchtouchevent(event); } @override public boolean ontouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: system.out.println("rtbutton---ontouchevent---down"); break; case motionevent.action_move: system.out.println("rtbutton---ontouchevent---move"); break; case motionevent.action_up: system.out.println("rtbutton---ontouchevent---up"); break; default: break; } return super.ontouchevent(event); } }
在rtbutton中我重写了dispatchtouchevent和ontouchevent方法,并获取了motionevent各个事件状态,打印输出了每一个状态下的信息。然后在activity_main.xml中直接在根布局下放入自定义的按钮rtbutton。
activity_main.xml
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/mylayout" android:layout_width="match_parent" android:layout_height="match_parent" > <com.ryantang.eventdispatchdemo.rtbutton android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="button"/> </linearlayout>
接下来在activity中为rtbutton设置ontouch和onclick的监听器来跟踪事件传递的过程,另外,activity中也有一个dispatchtouchevent方法和一个ontouchevent方法,我们也重写他们并输出打印信息。
mainactivity.java
public class mainactivity extends activity { private rtbutton button; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); button = (rtbutton)this.findviewbyid(r.id.btn); button.setontouchlistener(new ontouchlistener() { @override public boolean ontouch(view v, motionevent event) { switch (event.getaction()) { case motionevent.action_down: system.out.println("rtbutton---ontouch---down"); break; case motionevent.action_move: system.out.println("rtbutton---ontouch---move"); break; case motionevent.action_up: system.out.println("rtbutton---ontouch---up"); break; default: break; } return false; } }); button.setonclicklistener(new onclicklistener() { @override public void onclick(view v) { system.out.println("rtbutton clicked!"); } }); } @override public boolean dispatchtouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: system.out.println("activity---dispatchtouchevent---down"); break; case motionevent.action_move: system.out.println("activity---dispatchtouchevent---move"); break; case motionevent.action_up: system.out.println("activity---dispatchtouchevent---up"); break; default: break; } return super.dispatchtouchevent(event); } @override public boolean ontouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: system.out.println("activity---ontouchevent---down"); break; case motionevent.action_move: system.out.println("activity---ontouchevent---move"); break; case motionevent.action_up: system.out.println("activity---ontouchevent---up"); break; default: break; } return super.ontouchevent(event); } }
代码部分已经完成了,接下来运行工程,并点击按钮,查看日志输出信息,我们可以看到如下结果:
通过日志输出可以看到,首先执行了activity的dispatchtouchevent方法进行事件分发,在mainactivity.java代码第55行,dispatchtouchevent方法的返回值是super.dispatchtouchevent(event),因此调用了父类方法,我们进入activity.java的源码中看看具体实现。
activity.java
/** * called to process touch screen events. you can override this to * intercept all touch screen events before they are dispatched to the * window. be sure to call this implementation for touch screen events * that should be handled normally. * * @param ev the touch screen event. * * @return boolean return true if this event was consumed. */ public boolean dispatchtouchevent(motionevent ev) { if (ev.getaction() == motionevent.action_down) { onuserinteraction(); } if (getwindow().superdispatchtouchevent(ev)) { return true; } return ontouchevent(ev); }
从源码中可以看到,dispatchtouchevent方法只处理了actiondown事件,前面提到过,所有的事件都是以按下为起点的,所以,android认为当actiondown事件没有执行时,后面的事件都是没有意义的,所以这里首先判断action_down事件。如果事件成立,则调用了onuserinteraction方法,该方法可以在activity中被重写,在事件被分发前会调用该方法。该方法的返回值是void型,不会对事件传递结果造成影响,接着会判断getwindow().superdispatchtouchevent(ev)的执行结果,看看它的源码:
activity.java
/** * used by custom windows, such as dialog, to pass the touch screen event * further down the view hierarchy. application developers should * not need to implement or call this. * */ public abstract boolean superdispatchtouchevent(motionevent event);
通过源码注释我们可以了解到这是个抽象方法,用于自定义的window,例如自定义dialog传递触屏事件,并且提到开发者不需要去实现或调用该方法,系统会完成,如果我们在mainactivity中将dispatchtouchevent方法的返回值设为true,那么这里的执行结果就为true,从而不会返回执行ontouchevent(ev),如果这里返回false,那么最终会返回执行ontouchevent方法,由此可知,接下来要调用的就是ontouchevent方法了。别急,通过日志输出信息可以看到,action_down事件从activity被分发到了rtbutton,接着执行了ontouch和ontouchevent方法,为什么先执行ontouch方法呢?我们到rtbutton中的dispatchtouchevent看看view中的源码是如何处理的。
view.java
/** * pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event the motion event to be dispatched. * @return true if the event was handled by the view, false otherwise. */ public boolean dispatchtouchevent(motionevent event) { if (minputeventconsistencyverifier != null) { minputeventconsistencyverifier.ontouchevent(event, 0); } if (onfiltertoucheventforsecurity(event)) { //noinspection simplifiableifstatement listenerinfo li = mlistenerinfo; if (li != null && li.montouchlistener != null && (mviewflags & enabled_mask) == enabled && li.montouchlistener.ontouch(this, event)) { return true; } if (ontouchevent(event)) { return true; } } if (minputeventconsistencyverifier != null) { minputeventconsistencyverifier.onunhandledevent(event, 0); } return false; }
挑选关键代码进行分析,可以看代码第16行,这里有几个条件,当几个条件都满足时该方法就返回true,当条件li.montouchlistener不为空时,通过在源码中查找,发现montouchlistener是在以下方法中进行设置的。
view.java
/** * register a callback to be invoked when a touch event is sent to this view. * @param l the touch listener to attach to this view */ public void setontouchlistener(ontouchlistener l) { getlistenerinfo().montouchlistener = l; }
这个方法就已经很熟悉了,就是我们在mainactivity.java中为rtbutton设置的ontouchlistener,条件(mviewflags & enabled_mask) == enabled判断的是当前view是否是enable的,默认都是enable状态的。接着就是li.montouchlistener.ontouch(this, event)条件,这里调用了ontouch方法,该方法的调用就是我们在mainactivity.java中为rtbutton设置的监听回调,如果该方法返回true,则整个条件都满足,dispatchtouchevent就返回true,表示该事件就不继续向下分发了,因为已经被ontouch消费了。
如果ontouch返回的是false,则这个判断条件不成立,接着执行ontouchevent(event)方法进行判断,如果该方法返回true,表示事件被ontouchevent处理了,则整个dispatchtouchevent就返回true。到这里,我们就可以回答之前提出的“为什么先执行ontouch方法”的问题了。到目前为止,actiondown的事件经过了从activity到rtbutton的分发,然后经过ontouch和ontouchevent的处理,最终,actiondown事件交给了rtbutton得ontouchevent进行处理。
当我们的手(我这里用的genymotion然后用鼠标进行的操作,用手的话可能会执行一些actionmove操作)从屏幕抬起时,会发生actionup事件。从之前输出的日志信心中可以看到,actionup事件同样从activity开始到rtbutton进行分发和处理,最后,由于我们注册了onclick事件,当ontouchevent执行完毕后,就调用了onclick事件,那么onclick是在哪里被调用的呢?继续回到view.java的源代码中寻找。由于ontouchevent在view.java中的源码比较长,这里就不贴出来了,感兴趣的可以自己去研究一下,通过源码阅读,我们在actionup的处理分支中可以看到一个performclick()方法,从这个方法的源码中可以看到执行了哪些操作。
view.java
/** * call this view's onclicklistener, if it is defined. performs all normal * actions associated with clicking: reporting accessibility event, playing * a sound, etc. * * @return true there was an assigned onclicklistener that was called, false * otherwise is returned. */ public boolean performclick() { sendaccessibilityevent(accessibilityevent.type_view_clicked); listenerinfo li = mlistenerinfo; if (li != null && li.monclicklistener != null) { playsoundeffect(soundeffectconstants.click); li.monclicklistener.onclick(this); return true; } return false; }
在if分支里可以看到执行了li.monclicklistener.onclick(this);这句代码,这里就执行了我们为rtbutton实现的onclick方法,所以,到目前为止,可以回答前一个“onclick是在哪里被调用的呢?”的问题了,onclick是在ontouchevent中被执行的,并且,onclick要后于ontouch的执行。
到此,点击按钮的事件传递就结束了,我们结合源代码窥探了其中的执行细节,如果我们修改各个事件控制方法的返回值又会发生什么情况呢,带着这个问题,进入下一节的讨论。
android事件拦截
从上一节分析中,我们知道了在android中存在哪些事件类型,事件的传递过程以及在源码中对应哪些处理方法。我们可以知道在android中,事件是通过层级传递的,一次事件传递对应一个完整的层级关系,例如上节中分析的actiondown事件从activity传递到rtbutton,actionup事件也同样。结合源码分析各个事件处理的方法,也可以明确看到事件的处理流程。
之前提过,所有事件处理方法的返回值都是boolean类型的,现在我们来修改这个返回值,首先从activity开始,根据之前的日志输出结果,首先执行的是activity的dispatchtouchevent方法,现在将之前的返回值super.dispatchtouchevent(event)修改为true,然后重新编译运行并点击按钮,看到如下的日志输出结果。
可以看到,事件执行到dispatchtouchevent方法就没有再继续往下分发了,这也验证了之前的说法,返回true时,不再继续往下分发,从之前分析过的activity的dispatchtouchevent源码中也可知,当返回true时,就没有去执行ontouchevent方法了。
接着,将上述修改还原,让事件在activity这继续往下分发,接着就分发到了rtbutton,将rtbutton的dispatchtouchevent方法的返回值修改为true,重新编译运行并查看输出日志结果。
从结果可以看到,事件在rtbutton的dispatchtouchevent方法中就没有再继续往下分发了。接着将上述修改还原,将rtbutton的ontouchevent方法返回值修改为true,让其消费事件,根据之前的分析,onclick方法是在ontouchevent方法中被调用的,事件在这被消费后将不会调用onclick方法了,编译运行,得到如下日志输出结果。
跟分析结果一样,onclick方法并没有被执行,因为事件在rtbutton的ontouchevent方法中被消费了。下图是整个事件传递的流程图。
到目前为止,android中的事件拦截机制就分析完了。但这里我们只讨论了单布局结构下单控件的情况,如果是嵌套布局,那情况又是怎样的呢?接下来我们就在嵌套布局的情况下对android的事件传递机制进行进一步的探究和分析。
android嵌套布局事件传递
首先,新建一个类rtlayout继承于linearlayout,同样重写dispatchtouchevent和ontouchevent方法,另外,还需要重写onintercepttouchevent方法,在文章开头介绍过,这个方法只有在viewgroup和其子类中才存在,作用是控制是否需要拦截事件。这里不要和dispatchtouchevent弄混淆了,后者是控制对事件的分发,并且后者要先执行。
那么,事件是先传递到view呢,还是先传递到viewgroup的?通过下面的分析我们可以得出结论。首先,我们需要对工程代码进行一些修改。
rtlayout.java
public class rtlayout extends linearlayout { public rtlayout(context context, attributeset attrs) { super(context, attrs); } @override public boolean dispatchtouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: system.out.println("rtlayout---dispatchtouchevent---down"); break; case motionevent.action_move: system.out.println("rtlayout---dispatchtouchevent---move"); break; case motionevent.action_up: system.out.println("rtlayout---dispatchtouchevent---up"); break; default: break; } return super.dispatchtouchevent(event); } @override public boolean onintercepttouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: system.out.println("rtlayout---onintercepttouchevent---down"); break; case motionevent.action_move: system.out.println("rtlayout---onintercepttouchevent---move"); break; case motionevent.action_up: system.out.println("rtlayout---onintercepttouchevent---up"); break; default: break; } return super.onintercepttouchevent(event); } @override public boolean ontouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: system.out.println("rtlayout---ontouchevent---down"); break; case motionevent.action_move: system.out.println("rtlayout---ontouchevent---move"); break; case motionevent.action_up: system.out.println("rtlayout---ontouchevent---up"); break; default: break; } return super.ontouchevent(event); } }
同时,在布局文件中为rtbutton添加一个父布局,指明为自定义的rtlayout,修改后的布局文件如下。
activity_main.xml
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <com.ryantang.eventdispatchdemo.rtlayout android:id="@+id/mylayout" android:layout_width="match_parent" android:layout_height="match_parent" > <com.ryantang.eventdispatchdemo.rtbutton android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="button" /> </com.ryantang.eventdispatchdemo.rtlayout> </linearlayout>
最后,我们在activity中也为rtlayout设置ontouch和onclick事件,在mainactivity中添加如下代码。
mainactivity.java
rtlayout.setontouchlistener(new ontouchlistener() { @override public boolean ontouch(view v, motionevent event) { switch (event.getaction()) { case motionevent.action_down: system.out.println("rtlayout---ontouch---down"); break; case motionevent.action_move: system.out.println("rtlayout---ontouch---move"); break; case motionevent.action_up: system.out.println("rtlayout---ontouch---up"); break; default: break; } return false; } }); rtlayout.setonclicklistener(new onclicklistener() { @override public void onclick(view v) { system.out.println("rtlayout clicked!"); } });
代码修改完毕后,编译运行工程,同样,点击按钮,查看日志输出结果如下:
从日志输出结果我们可以看到,嵌套了rtlayout以后,事件传递的顺序变成了activity->rtlayout->rtbutton,这也就回答了前面提出的问题,android中事件传递是从viewgroup传递到view的,而不是反过来传递的。
从输出结果第三行可以看到,执行了rtlayout的onintercepttouchevent方法,该方法的作用就是判断是否需要拦截事件,我们到viewgroup的源码中看看该方法的实现。
viewgroup.java
public boolean onintercepttouchevent(motionevent ev) { return false; }
该方法的实现很简单,只返回了一个false。那么这个方法是在哪被调用的呢,通过日志输出分析可知它是在rtlayout的dispatchtouchevent执行后执行的,那我们就进到dispatchtouchevent源码里面去看看。由于源码比较长,我将其中的关键部分截取出来做解释说明。
viewgroup.java
// check for interception. final boolean intercepted; if (actionmasked == motionevent.action_down || mfirsttouchtarget != null) { final boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != 0; if (!disallowintercept) { intercepted = onintercepttouchevent(ev); ev.setaction(action); // restore action in case it was changed } else { intercepted = false; } } else { // there are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
从这部分代码中可以看到onintercepttouchevent调用后返回值被赋值给intercepted,该变量控制了事件是否要向其子控件分发,所以它起到拦截的作用,如果onintercepttouchevent返回false则不拦截,如果返回true则拦截当前事件。我们现在将rtlayout中的该方法返回值修改为true,并重新编译运行,然后点击按钮,查看输出结果如下。
可以看到,我们明明点击的按钮,但输出结果显示rtlayout点击事件被执行了,再通过输出结果分析,对比上次的输出结果,发现本次的输出结果完全没有rtbutton的信息,没错,由于onintercepttouchevent方法我们返回了true,在这里就将事件拦截了,所以他不会继续分发给rtbutton了,反而交给自身的ontouchevent方法执行了,理所当然,最后执行的就是rtlayout的点击事件了。
总结
以上我们对android事件传递机制进行了分析,期间结合系统源码对事件传递过程中的处理情况进行了探究。通过单布局情况和嵌套布局情况下的事件传递和处理进行了分析,现总结如下:
android中事件传递按照从上到下进行层级传递,事件处理从activity开始到viewgroup再到view。
事件传递方法包括dispatchtouchevent、ontouchevent、onintercepttouchevent,其中前两个是view和viewgroup都有的,最后一个是只有viewgroup才有的方法。这三个方法的作用分别是负责事件分发、事件处理、事件拦截。
ontouch事件要先于onclick事件执行,ontouch在事件分发方法dispatchtouchevent中调用,而onclick在事件处理方法ontouchevent中被调用,ontouchevent要后于dispatchtouchevent方法的调用。
上一篇: 牛客刷题后,我哭了。(多线程篇(一))
推荐阅读