事件分发是Android中非常重要的机制,是用户与界面交互的基础。这篇文章将通过示例打印出的Log,绘制出事件分发的流程图,让大家更容易的去理解Android的事件分发机制。
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 |
这篇文章中我们只考虑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来绘制一张事件传递的流程图
现在来理一下事件序列的流程:
这里使用工作中的情况来模拟一下:老板(Activity)、项目经理(ViewGroup)、软件工程师(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事件传递的流程图
我们来整理下这个事件序列的流程:
这里使用工作中的情况来模拟:依旧是老板(Activity)、项目经理(ViewGroup)、软件工程师(View)
从老板交任务给项目经理,项目经理交任务给工程师,这一段流程和之前的例子相同。不同之处是软件工程师没有完成这个任务(View#onTouchEvent返回false),告诉项目经理我没有完成,然后项目经理自己进行了尝试,同样没有完成(ViewGroup#onTouchEvent返回false),项目经理告诉了老板,我没有完成,然后老板自己试了下也没有完成这个任务(Activity#onTouchEvent返回false),但之后的也有项目的二期、三期,不过老板知道你们完成不了,所以都是他自己进行尝试,不过很惨都没完成。(这段有点与正常情况不同,不过只是打个比方)
通过结合上面两个例子,可以得出一些结论:
事件分发中拦截的情况,这里我把它分为2种,一种是在ACTION_DOWN事件时,就进行拦截的;另一种是在ACTION_DOWN之后的事件序列中,对事件进行了拦截。
为了达到在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)的自己来做。
通过上面的例子,可以得出一些结论:
这里把使用的示例恢复到初始状态,然后把我在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)。
从这里也可以得出一个结论:
本文通过示例打印出的各种Log对Android的事件分发机制进行,得出如下结论。
事件相关方法 | 方法功能 | Activity | ViewGroup | View |
---|---|---|---|---|
public boolean dispatchTouchEvent | 事件分发 | Yes | Yes | Yes |
public boolean onInterceptTouchEvent | 事件拦截 | No | Yes | No |
public boolean onTouchEvent | 事件消费 | Yes | Yes | Yes |
如果在阅读过程中,有任何疑问与问题,欢迎与我联系。
GitHub: https://github.com/Idtk
博客:http://www.idtkm.com
微博: http://weibo.com/Idtk
邮箱: Idtkma@gmail.com