理解并测试什么是Android事件分发
一、什么是事件分发
所谓事件分发,就是将一次完整的点击所包含的点击事件传递到某个具体的view或viewgroup,让该view或该viewgroup处理它(消费它)。分发是从上往下(父到子)依次传递的,其中可能经过的对象有最上层activity,中间层viewgroup,最下层view。
二、activity的层次结构
源码查找:
1.自己的activity的setcontentview()
方法
@override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_event_distribution); }
2.跳转到activity.java的setcontentview()
方法,可以看到,调用了getwindow()
的方法
public void setcontentview(@layoutres int layoutresid) { getwindow().setcontentview(layoutresid); initwindowdecoractionbar(); }
3.activity.java的mwindow来自phonewindow
mwindow = new phonewindow(this, window, activityconfigcallback);
4.phonewindow.java-->setcontentview()--> installdecor(),在phonewindow中调用了installdecor()
方法
@override public void setcontentview(int layoutresid) { // note: feature_content_transitions may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. do not check the feature // before this happens. if (mcontentparent == null) { installdecor(); //继续执行 } else if (!hasfeature(feature_content_transitions)) { mcontentparent.removeallviews(); } ..................
5.phonewindow.java-->setcontentview()--> installdecor()--> generatelayout(mdecor),在 installdecor()中又继续执行了generatelayout(mdecor)
方法。
mcontentparent = generatelayout(mdecor);
6.phonewindow.java-->generatelayout()
viewgroup generatelayout(decorview decor)
7.phonewindow.java-->generatelayout()--> int layoutresource,layoutresource
根据不同情况,返回不同的资源文件,也就是布局文件。
int layoutresource;
8.phonewindow.java-->generatelayout()-->r.layout.screen_title; 拿出一个常用的布局文件,screen_title.xml
layoutresource = r.layout.screen_title;
9.screen_title.xml的代码, viewstub
是用来显示actionbar
的,另外两个framelayout,一个显示titleview
,一个显示contentview,平时写的内容,正是contentview
。
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:fitssystemwindows="true"> <!-- popout bar for action modes --> <viewstub android:id="@+id/action_mode_bar_stub" android:inflatedid="@+id/action_mode_bar" android:layout="@layout/action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="?attr/actionbartheme" /> <framelayout android:layout_width="match_parent" android:layout_height="?android:attr/windowtitlesize" style="?android:attr/windowtitlebackgroundstyle"> <textview android:id="@android:id/title" style="?android:attr/windowtitlestyle" android:background="@null" android:fadingedge="horizontal" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="match_parent" /> </framelayout> <framelayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="1" android:foregroundgravity="fill_horizontal|top" android:foreground="?android:attr/windowcontentoverlay" /> </linearlayout>
如以下结构图:
三、事件分发涉及到的主要方法
涉及到的方法
@override public boolean dispatchtouchevent(motionevent ev) { //分发事件 return super.dispatchtouchevent(ev); } @override public boolean onintercepttouchevent(motionevent ev) { //拦截事件 return super.onintercepttouchevent(ev); } @override public boolean ontouchevent(motionevent event) { //消费事件 return super.ontouchevent(event); }
activity涉及到的方法:dispatchtouchevent()、ontouchevent()
viewgroup涉及到的方法:dispatchtouchevent()、onintercepttouchevent()
view涉及到的方法:dispatchtouchevent()、ontouchevent()
四、事件分发流程
1.activity把事件分发到viewgroup
(1)事件传递
每一次事件分发,都是从dispatchtouchevent()开始的。
1)查看activity的源码,调用了getwindow().superdispatchtouchevent(ev)
public boolean dispatchtouchevent(motionevent ev) { if (ev.getaction() == motionevent.action_down) { onuserinteraction(); } if (getwindow().superdispatchtouchevent(ev)) { return true; } return ontouchevent(ev); }
2)在activity.java中可以看到,所以getwindow().superdispatchtouchevent(ev)
实际上是调用了phonewindow.java中的superdispatchtouchevent(ev)
方法。
public window getwindow() { return mwindow; } mwindow = new phonewindow(this, window, activityconfigcallback); //mwindow的定义
3)然后再看phonewindow.java中的superdispatchtouchevent(ev)
方法,是调用decorview.java的mdecor.superdispatchtouchevent(event)
@override public boolean superdispatchtouchevent(motionevent event) { return mdecor.superdispatchtouchevent(event); }
4)而decorview是继承framelayout,再继承viewgroup的
private decorview mdecor; //实例对象 class decorview extends framelayout; //继承framelayout framelayout extends viewgroup; //继承viewgroup
5)从上面四步来分析,avtivity的getwindow().superdispatchtouchevent()
方法最后调用的是viewgroup的dispatchtouchevent()
方法,从而实现了事件从activity的dispatchtouchevent()
向下传递到viewgroup的dispatchtouchevent()
方法。
(2)总结
6)返回值分析。
-
如果avtivity的
getwindow().superdispatchtouchevent()
返回true,则avtivity的dispatchtouchevent(),也会返回true,表示点击事件顺利分发给viewgroup,由viewgroup继续进行下一层的分发,avtivity的分发任务结束。 -
如果返回false,表示此次点击事件由avtivity层消费,会执行avtivity的
ontouchevent()
,无论ontouchevent()
这个方法返回的是true或者false,本次的事件分发都结束了。
(3)流程图
2.viewgroup把事件分发到viewgroup或view
(1)事件拦截
viewgroup.java中的部分代码
viewgroup-->dispatchtouchevent()
public boolean dispatchtouchevent(motionevent ev) { if (!disallowintercept) { intercepted = onintercepttouchevent(ev); ev.setaction(action); // restore action in case it was changed } else { intercepted = false; } }
方法中使用了onintercepttouchevent(ev)
方法
- 如果返回true,则表示viewgroup拦截此次事件。
- 如果返回false,则表示viewgroup不拦截,事件继续往下分发。
-
onintercepttouchevent(ev)
默认返回不拦截,可以在viewgroup中重写改方法来拦截事件。 - 不拦截事件,则会调用viewgroup的ontouchevent()来处理点击事件,把事件消费掉。
(2)分发
这个源码中,使用到了intercepted这个变量,主要作用是来遍历子viewgroup和view,
- 当intercepted为false的时候,遍历子viewgroup和子view,因为这个事件没有被消费掉,继续分发到子viewgroup和子view。
- 当intercepted为true的时候,该事件已经被消费,不会继续往下分发,也不会遍历子viewgroup和子view,也不会执行if语句里面的方法。
- 进入if语句中判断点击事件的触摸范围(焦点)是否属于某个子viewgroup或者子view。
- 如果触摸范围属于子view,则调用子view的dispatchtouchevent()方法。
- 如果触摸范围属于子viewgroup,则继续遍历下一层的viewgroup或者view。
- 遍历到最下层的view,还是找不到消费此处事件的view,则依次回调上一层的viewgroup的ontouchevent()方法,直到回调到activity的ontouchevent()方法。
// check for interception. final boolean intercepted; if (!canceled && !intercepted) { // if the event is targeting accessibility focus we give it to the // view that has accessibility focus and if it does not handle it // we clear the flag and dispatch the event to all children as usual. // we are looking up the accessibility focused host to avoid keeping // state since these events are very rare. view childwithaccessibilityfocus = ev.istargetaccessibilityfocus() ? findchildwithaccessibilityfocus() : null;
(3)流程图
3.view的事件分发
(1)分析
view的dispatchtouchevent()的源码
public boolean dispatchtouchevent(motionevent event) { // if the event should be handled by accessibility focus first. if (event.istargetaccessibilityfocus()) { // we don't have focus or no virtual descendant has it, do not handle the event. if (!isaccessibilityfocusedvieworhost()) { return false; } // we have focus and got the event, then use normal event dispatch. event.settargetaccessibilityfocus(false); } boolean result = false; if (minputeventconsistencyverifier != null) { minputeventconsistencyverifier.ontouchevent(event, 0); } final int actionmasked = event.getactionmasked(); if (actionmasked == motionevent.action_down) { // defensive cleanup for new gesture stopnestedscroll(); } if (onfiltertoucheventforsecurity(event)) { if ((mviewflags & enabled_mask) == enabled && handlescrollbardragging(event)) { result = true; } //noinspection simplifiableifstatement listenerinfo li = mlistenerinfo; if (li != null && li.montouchlistener != null && (mviewflags & enabled_mask) == enabled && li.montouchlistener.ontouch(this, event)) { result = true; } if (!result && ontouchevent(event)) { result = true; } } if (!result && minputeventconsistencyverifier != null) { minputeventconsistencyverifier.onunhandledevent(event, 0); } // clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an action_down but we didn't want the rest // of the gesture. if (actionmasked == motionevent.action_up || actionmasked == motionevent.action_cancel || (actionmasked == motionevent.action_down && !result)) { stopnestedscroll(); } return result; }
-
在view的dispatchtouchevent()方法中首先会调用
ontouch()
方法,如果ontouch()方法能够消费该事件,就会直接返回true,从而直接结束view的dispatchtouchevent()方法,不再执行ontouchevent()方法; -
如果ontouch()方法不能消费该事件,就会返回false,从而继续执行
ontouchevent``()
方法。 - 如果ontouchevent()能够消费该事件,就会返回true从而直接结束dispatchtouchevent()方法。
-
如果ontouchevent()方法也不能消费该事件,就会返回默认的false从而回调到上一层
viewgroup
的ontouchevent()方法,直到回调到activity的ontouchevent``()
方法。
(2)流程图
五、具体例子
(0)测试代码
共有三种类型和四个测试代码
activity:eventdistributionactivity
viewgroup:eventdistributionlinearlayout1、eventdistributionlinearlayout2
view:eventdistributionbutton
分别代码:
eventdistributionactivity.java
public class eventdistributionactivity extends baseactivity { button mbtn; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_event_distribution); mbtn = findviewbyid(r.id.btn); onclick(); } public void onclick() { mbtn.setonclicklistener(new view.onclicklistener() { @override public void onclick(view v) { log.v("showlog", "按钮被点击!"); } }); mbtn.setontouchlistener(new view.ontouchlistener() { @override public boolean ontouch(view v, motionevent event) { boolean dis = false; log.v("showlog", "button.touch()=" + dis); return dis; } }); } @override public boolean dispatchtouchevent(motionevent ev) { //分发事件 boolean dis = super.dispatchtouchevent(ev); log.v("showlog", "activity.dispatchtouchevent()=" + dis); return dis; } @override public boolean ontouchevent(motionevent event) { //处理事件 boolean dis = super.ontouchevent(event); log.v("showlog", "activity.ontouchevent()=" + dis); return dis; } }
eventdistributionlinearlayout1.java
public class eventdistributionlinearlayout1 extends linearlayout { public eventdistributionlinearlayout1(context context, attributeset attrs) { super(context, attrs); } @override public boolean dispatchtouchevent(motionevent ev) { //分发事件 boolean dis = super.dispatchtouchevent(ev); log.v("showlog", "linearlayout1.dispatchtouchevent()=" + dis); return dis; } @override public boolean onintercepttouchevent(motionevent ev) { //拦截事件 boolean dis = super.onintercepttouchevent(ev); log.v("showlog", "linearlayout1.onintercepttouchevent()=" + dis); return dis; } @override public boolean ontouchevent(motionevent event) { //消费事件 boolean dis = super.ontouchevent(event); log.v("showlog", "linearlayout1.ontouchevent()=" + dis); return dis; } }
eventdistributionlinearlayout2.java
public class eventdistributionlinearlayout2 extends linearlayout { public eventdistributionlinearlayout2(context context, attributeset attrs) { super(context, attrs); } @override public boolean dispatchtouchevent(motionevent ev) { //分发事件 boolean dis = super.dispatchtouchevent(ev); log.v("showlog", "linearlayout2.dispatchtouchevent()=" + dis); return dis; } @override public boolean onintercepttouchevent(motionevent ev) { //拦截事件 boolean dis = super.onintercepttouchevent(ev); dis = true; log.v("showlog", "linearlayout2.onintercepttouchevent()=" + dis); return dis; } @override public boolean ontouchevent(motionevent event) { //消费事件 boolean dis = super.ontouchevent(event); log.v("showlog", "linearlayout2.ontouchevent()=" + dis); return dis; } }
eventdistributionbutton.java
public class eventdistributionbutton extends button { public eventdistributionbutton(context context, attributeset attrs) { super(context, attrs); } @override public boolean dispatchtouchevent(motionevent event) { //分发事件 boolean dis = super.dispatchtouchevent(event); log.v("showlog", "button.dispatchtouchevent()=" + dis); return dis; } @override public boolean ontouchevent(motionevent event) { //消费事件 boolean dis = super.ontouchevent(event); log.v("showlog", "button.ontouchevent()=" + dis); return dis; } @override public boolean performclick() { boolean dis = super.performclick(); log.v("showlog", "button.performclick()="+dis); return dis; } }
activity_event_distribution.xml
<?xml version="1.0" encoding="utf-8"?> <com.lanjiabin.systemtest.event.eventdistributionlinearlayout1 xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".event.eventdistributionactivity"> <com.lanjiabin.systemtest.event.eventdistributionlinearlayout2 android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.lanjiabin.systemtest.event.eventdistributionbutton android:background="@drawable/button_color_circle_shape1" android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_margintop="300dp" android:text="点击" /> </com.lanjiabin.systemtest.event.eventdistributionlinearlayout2> </com.lanjiabin.systemtest.event.eventdistributionlinearlayout1>
效果图:一个linearlayout1包含linearlayout2再包含一个button
界面只有一个按钮
(1)测试1
测试用例:按钮消费事件,和空白处不消费事件
按住按钮不松开,事件被button的ontouchevent()消费
linearlayout1.onintercepttouchevent()=false linearlayout2.onintercepttouchevent()=false button.touch()=false button.ontouchevent()=true button.dispatchtouchevent()=true linearlayout2.dispatchtouchevent()=true linearlayout1.dispatchtouchevent()=true activity.dispatchtouchevent()=true
按住空白处不松开,没有事件被消费
linearlayout1.onintercepttouchevent()=false linearlayout2.onintercepttouchevent()=false linearlayout2.ontouchevent()=false linearlayout2.dispatchtouchevent()=false linearlayout1.ontouchevent()=false linearlayout1.dispatchtouchevent()=false activity.ontouchevent()=false activity.dispatchtouchevent()=false
(2)测试2
测试用例:在linearlayout2处截断
修改代码:eventdistributionlinearlayout2.java
@override public boolean onintercepttouchevent(motionevent ev) { //拦截事件 boolean dis = super.onintercepttouchevent(ev); dis = true; log.v("showlog", "linearlayout2.onintercepttouchevent()=" + dis); return dis; }
按住按钮不松开:事件截断生效,将不会继续遍历下层的viewgroup或者view,所以日志中看不到button的日志打印。
linearlayout1.onintercepttouchevent()=false linearlayout2.onintercepttouchevent()=true //截断生效 linearlayout2.ontouchevent()=false linearlayout2.dispatchtouchevent()=false linearlayout1.ontouchevent()=false linearlayout1.dispatchtouchevent()=false activity.ontouchevent()=false activity.dispatchtouchevent()=false
(3)测试3
测试用例:在view中ontouch()中返回true
也就是在button中设置ontouch()返回true,则不会产生点击事件,完整的点击事件是被按下和松开的,所以上面没有点击按钮的监听事件的打印日志。
首先,看看完整的点击事件日志,去掉先前测试的改变的代码。
linearlayout1.onintercepttouchevent()=false linearlayout2.onintercepttouchevent()=false button.touch()=false button.ontouchevent()=true //触摸按下事件被消费 button.dispatchtouchevent()=true linearlayout2.dispatchtouchevent()=true linearlayout1.dispatchtouchevent()=true activity.dispatchtouchevent()=true //触摸按下的事件处理结束 linearlayout1.onintercepttouchevent()=false //开始触摸i抬起的事件 linearlayout2.onintercepttouchevent()=false button.touch()=false button.ontouchevent()=true //触摸抬起的事件被消费 button.dispatchtouchevent()=true linearlayout2.dispatchtouchevent()=true linearlayout1.dispatchtouchevent()=true activity.dispatchtouchevent()=true 按钮被点击! //onclick button.performclick()=true
开始测试用例:
修改代码:
eventdistributionactivity.java,将boolean dis = false;
修改为boolean dis = true;
mbtn.setontouchlistener(new view.ontouchlistener() { @override public boolean ontouch(view v, motionevent event) { boolean dis = true; log.v("showlog", "button.touch()=" + dis); return dis; } });
按下和松开按钮:可以看到,事件被button.touch()消费了,因为在touch()返回了true,事件没有继续传递下去,所以onclick事件没有被触发,没有生效。
linearlayout1.onintercepttouchevent()=false linearlayout2.onintercepttouchevent()=false button.touch()=true //触摸事件被消费 button.dispatchtouchevent()=true linearlayout2.dispatchtouchevent()=true linearlayout1.dispatchtouchevent()=true activity.dispatchtouchevent()=true //触摸按下事件处理完毕 linearlayout1.onintercepttouchevent()=false linearlayout2.onintercepttouchevent()=false button.touch()=true button.dispatchtouchevent()=true linearlayout2.dispatchtouchevent()=true linearlayout1.dispatchtouchevent()=true activity.dispatchtouchevent()=true
编程中我们会遇到多少挫折?表放弃,沙漠尽头必是绿洲。
原文:https://www.cnblogs.com/lanjiabin/p/12853407.html
上一篇: Flask目录结构
下一篇: Leetcode 5. 最长回文子串