Android 事件分发详解及示例代码
事件分发是android中非常重要的机制,是用户与界面交互的基础。这篇文章将通过示例打印出的log,绘制出事件分发的流程图,让大家更容易的去理解android的事件分发机制。
一、必要的基础知识
1、相关方法
android中与事件分发相关的方法主要包括dispatchtouchevent、onintercepttouchevent、ontouchevent三个方法,而事件分发一般会经过三种容器,分别为activity、viewgroup、view。下表对这三种容器分别拥有的事件分发相关方法进行了整理。
事件相关方法 | 方法功能 | activity | viewgroup | view |
---|---|---|---|---|
public boolean dispatchtouchevent | 事件分发 | yes | yes | yes |
public boolean onintercepttouchevent | 事件拦截 | no | yes | no |
public boolean ontouchevent | 事件消费 | yes | yes | yes |
分发: dispatchtouchevent如果返回true,则表示在当前view或者其子view(子子…view)中,找到了处理事件的view;反之,则表示没有寻找到
拦截: onintercepttouchevent如果返回true,则表示这个事件由当前view进行处理,不管处理结果如何,都不会再向子view传递这个事件;反之,则表示当前view不主动处理这个事件,除非他的子view返回的事件分发结果为false
消费: ontouchevent如果返回true,则表示当前view就是事件传递的终点;反之,则表示当前view不是事件传递的终点
2、相关事件
这篇文章中我们只考虑4种触摸事件: action_down、action_up、action_move、action_canal。 事件序列:一个事件序列是指从手指触摸屏幕开始,到手指离开屏幕结束,这个过程中产生的一系列事件。一个事件序列以action_down事件开始,中间可能经过若干个move,以action_up事件结束。 接下来我们将使用之前的文章自定义view——弹性滑动中例子来作为本文的示例,简单增加一些代码即可,修改之后的代码请点击查看。
二、示例的默认情况
我们可以从示例代码的xml中看出,图片都是可点击的。
<?xml version="1.0" encoding="utf-8"?> <com.idtk.customscroll.parentview 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" android:padding="10dp" tools:context="com.idtk.customscroll.mainactivity" > <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/zhiqinchun" android:clickable="true"/> <com.idtk.customscroll.childview android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/hanzhan" android:clickable="true"/> <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/shengui" android:clickable="true"/> <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/dayu" android:clickable="true"/> </com.idtk.customscroll.parentview>
我们现在来点击一下,查看下打印出的日志。
根据打印出的log来绘制一张事件传递的流程图
现在来理一下事件序列的流程:
- action_down事件从activity#dispatchtouchevent方法开始
- action_down事件传递至viewgroup#dispatchtouchevent方法,viewgroup#onintercepttouchevent返回false,表示不拦截action_down
- action_down事件传递到view#dispatchtouchevent方法,在view#ontouchevent进行执行,返回true,表示事件已经被消费
- 返回的结果true,被回传到view#dispatchtouchevent,之后回传到action_down事件的起点activity#dispatchtouchevent方法
- action_up事件的传递过程与action_down相同,这里不再复述
这里使用工作中的情况来模拟一下:老板(activity)、项目经理(viewgroup)、软件工程师(view)
老板分配一个任务给项目经理(activity#dispatchtouchevent → viewgroup#dispatchtouchevent),项目经理选择自己不做这个任务(viewgroup#dispatchtouchevent返回false),交由软件工程师处理这个任务(<view#dispatchtouchevent)(我们忽略总监与组长的情况),软件工程师完成了这个任务(view#ontouchevent返回true)
把结果告诉项目经理(返回结果true,view#dispatchtouchevent→ viewgroup#dispatchtouchevent),项目经理把结果告诉老板(返回结果true,viewgroup#dispatchtouchevent→activity#dispatchtouchevent)
项目经理完成的不错,老板决定把这个项目的二期、三期等都交给项目经理,同样项目经理也觉得这个软件工程师完成的不错,所以也把二期、三期等都交给这个工程师来做
通过上面的传递过程,我们可以得出一些结论:
- 某个viewgroup如果onintercepttouchevent返回为false,则表示viewgroup不拦截事件,而是将其传递给view#dispatchtouchevent方法
- 事件总是由父元素分发给子元素
- 某个view如果ontouchevent返回true,表示事件被消费,则其结果将直接通过dispatchtouchevent方法传递回activity
- 如果某个view消费了action_down事件,那么这个事件序列中的后续事件也将交由其进行处理(有一些特殊情况除外,比如在序列中的之后事件进行拦截)
三、在view中不消费事件
我们现在修改示例代码的xml部分,android:clickable="true"全部修改为android:clickable="false"
<?xml version="1.0" encoding="utf-8"?> <com.idtk.customscroll.parentview 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" android:padding="10dp" tools:context="com.idtk.customscroll.mainactivity" > <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/zhiqinchun" android:clickable="false"/> <com.idtk.customscroll.childview android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/hanzhan" android:clickable="false"/> <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/shengui" android:clickable="false"/> <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/dayu" android:clickable="false"/> </com.idtk.customscroll.parentview>
这时再点击一下,查看新打印出的日志
现在根据log中显示的逻辑,分别绘制action_down事件与action_up事件传递的流程图
我们来整理下这个事件序列的流程:
- action_down事件的传递与之前相同,不同的地方在于,返回值的传递
- 因为不可点击,view#ontouchevent返回值为false,将其传递给自己的dispatchtouchevent方法,之后传递到viewgroup#dispatchtouchevent方法,再传递到viewgroup#ontouchevent方法
- viewgroup返回false之后,action_down事件交由activity#ontouchevent方法进行处理,然而依旧返回false,最后action_down事件的返回结果即为false
- action_up事件在发现view、viewgroup并不处理action_down事件后,直接将其传递给了activity#ontouch方法处理,处理返回false,action_up事件的返回结果即为false
这里使用工作中的情况来模拟:依旧是老板(activity)、项目经理(viewgroup)、软件工程师(view) 从老板交任务给项目经理,项目经理交任务给工程师,这一段流程和之前的例子相同。不同之处是软件工程师没有完成这个任务(view#ontouchevent返回false),告诉项目经理我没有完成,然后项目经理自己进行了尝试,同样没有完成(viewgroup#ontouchevent返回false),项目经理告诉了老板,我没有完成,然后老板自己试了下也没有完成这个任务(activity#ontouchevent返回false),但之后的也有项目的二期、三期,不过老板知道你们完成不了,所以都是他自己进行尝试,不过很惨都没完成。(这段有点与正常情况不同,不过只是打个比方)
通过结合上面两个例子,可以得出一些结论:
- 某个view如果ontouchevent返回false,表示事件没有被消费,则事件将传递给其父view的ontouchevent进行处理
- 某个view如果它不消耗action_down事件,那么这个序列的后续事件也不会再交由它来处理
- 如果事件没有view对其进行处理,那么最后将有activity进行处理
- view默认的ontouchevent在view可点击的情况下,将会消耗事件,返回true;不可点击的情况下,则不消耗事件,返回false(longclickable的情况,读者可以自行测试,结果与clickable相同)
四、在viewgroup中拦截事件
事件分发中拦截的情况,这里我把它分为2种,一种是在action_down事件时,就进行拦截的;另一种是在action_down之后的事件序列中,对事件进行了拦截。
1、在事件开始时拦截
为了达到在viewgroup中,一开始就拦截触摸事件的效果,我们需要进行修改,在parentview#onintercepttouchevent方法的最后部分,我注释掉的intercept=true;进行恢复,然后为activity_main.xml中的parentview增加android:clickable="true"属性。
<?xml version="1.0" encoding="utf-8"?> <com.idtk.customscroll.parentview 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" android:padding="10dp" tools:context="com.idtk.customscroll.mainactivity" > <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/zhiqinchun" android:clickable="true"/> <com.idtk.customscroll.childview android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/hanzhan" android:clickable="true"/> <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/shengui" android:clickable="true"/> <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/dayu" android:clickable="true"/> </com.idtk.customscroll.parentview>
我们现在来看下拦截情况下的事件流程图
这里大部分和之前的例子相同,主要的区别是在于viewgroup#onintercepttouchevent方法中,对传递的事件进行了拦截,返回true,action_down事件就传递到了viewgroup#ontouchevent中进行处理,action_down事件之后的传递就与之前的例子相同了。另一点重要的区别是,在viewgroup拦截下事件之后,此事件序列的其余事件,在进入viewgroup#dispatchtouchevent方法之后,不在需要进行是否拦截事件的判断,而是直接进入了ontouchevent方法之中。
使用工作中的情况来模拟:老板(activity)、项目经理(viewgroup)、软件工程师(view) 老板吧任务交给项目经理,项目经理认为这个项目比较难,所以决定自己处理(viewgroup#onintercepttouchevent,return true),项目经理比较厉害他把任务完成了(viewgroup#ontouchevent,return true),然后他告诉老板他完成了(return true,viewgroup#dispatchtouchevent→activity#dispatchtouchevent)。之后老板依旧会把任务交给项目经理,项目经理知道这个任务难度,所以不假思索(也就是这个事件序列中的其余事件没有经过viewgroup#onintercepttouchevent)的自己来做。
通过上面的例子,可以得出一些结论:
某个viewgroup如果onintercepttouchevent返回为true,则viewgroup拦截事件,将事件传递给其ontouchevent方法进行处理
某个viewgroup如果它的onintercepttouchevent返回为true,那么这个事件序列中的后续事件,不会在进行onintercepttouchevent的判断,而是由它的dispatchtouchevent方法直接传递给ontouchevent方法进行处理
2、在事件序列中拦截
这里把使用的示例恢复到初始状态,然后把我在parentview#onintercepttouchevent方法,switch内的两个注释掉的intercept = true;代码进行恢复,最后部分intercept = true;再次注释掉。
<?xml version="1.0" encoding="utf-8"?> <com.idtk.customscroll.parentview 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" android:padding="10dp" tools:context="com.idtk.customscroll.mainactivity" > <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/zhiqinchun" android:clickable="true"/> <com.idtk.customscroll.childview android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/hanzhan" android:clickable="true"/> <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/shengui" android:clickable="true"/> <com.idtk.customscroll.childview android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/dayu" android:clickable="true"/> </com.idtk.customscroll.parentview>
重新运行之后,滑动一个图片,来看看log
这里分成两张图片,是因为中间有很多action_move,为了方便观察,所以只截取了log的首尾部分。 这里的关键部分,就是红框中的action_cancel,可以看到action_down事件的传递时onintercepttouchevent并没有拦截,返回false,在其后的事件action_move再次进入onintercepttouchevent时,viewgroup对事件进行了拦截,这样将会对view传递一个action_cancel事件,之后的action_move事件就不再传递给view了。
使用工作中的情况来模拟:老板(activity)、项目经理(viewgroup)、软件工程师(view) 这里的情况就是,一期的任务和第一个例子一样的情况一样,由软件工程师完成,不过忽然项目经理觉得二期的任务有点难,然后决定自己完成。这时就给工程师说,这个项目的后续任务,不要你来完成了(action_cancel)。
从这里也可以得出一个结论:
某个view接收了action_down之后,这个序列的后续事件中,如果在某一刻被父view拦截了,则这个字view会收到一个action_cancel事件,并且也不会再收到这个事件序列中的后续事件。
五、小结
本文通过示例打印出的各种log对android的事件分发机制进行,得出如下结论。
- 一个事件序列是指从手指触摸屏幕开始,到手指离开屏幕结束,这个过程中产生的一系列事件。一个事件序列以action_down事件开始,中间可能经过若干个move,以action_up事件结束。
- 事件的传递过程是由外向内的,即事件总是由父元素分发给子元素
- 如果某个view消费了action_down事件,那么通常情况下,这个事件序列中的后续事件也将交由其进行处理,但可以通过调用其父view的onintercepttouchevent方法,对后续事件进行拦截
- 如果某个view它不消耗action_down事件,那么这个序列的后续事件也不会再交由它来处理
- 如果事件没有view对其进行处理,那么最后将有activity进行处理
- 如果事件传递的结果为true,回传的结果直接通过不断调用父view#dispatchtouchevent方法,传递给activity;如果事件传递的结果为false,回传的结果不断调用父view#ontouchevent方法,获取返回结果。
- view默认的ontouchevent在view可点击的情况下,将会消耗事件,返回true;不可点击的情况下,则不消耗事件,返回false(longclickable的情况,读者可以自行测试,结果与clickable相同)
- 如果某个viewgroup的onintercepttouchevent返回为true,那么这个事件序列中的后续事件,不会在进行onintercepttouchevent的判断,而是由它的dispatchtouchevent方法直接传递给ontouchevent方法进行处理
- 如果某个view接收了action_down之后,这个序列的后续事件中,在某一刻被父view拦截了,则这个字view会收到一个action_cancel事件,并且也不会再收到这个事件序列中的后续事件
事件相关方法 | 方法功能 | activity | viewgroup | view |
---|---|---|---|---|
public boolean dispatchtouchevent | 事件分发 | yes | yes | yes |
public boolean onintercepttouchevent | 事件拦截 | no | yes | no |
public boolean ontouchevent | 事件消费 | yes | yes | yes |
以上就是对android 事件分发的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!