1. 一个小问题引发的思考
2. 通过源码探索View中的事件分发机制
3.通过源码探索ViewGroup的事件分发机制
最近的一个项目中涉及,布局为一个RelativeLayout包含了一个EditText和一个Button,当点击EditText时,弹出软键盘,点击RelativeLayout中除了EditText和Button之外其它的地方时,收起软键盘。
实现起来很简单,为EditText和RelativeLayout分别注册一个onTouch事件,为Button注册一个click事件,一切Ok~ 但在业务层简单的实现背后,却给我带来了一些疑问:
1. EditText作为RelativeLayout的子元素,为何它的onTouch事件没有触发父元素(RelativeLayout)的onTouch事件,父子节点的同一事件的事件分发逻辑是怎样呢?
2. onClick事件和onTouch事件有和关联呢?
3. 我们既可以为控件注册onTouch事件(setOnTouchLisnter),也可以自定义控件实现onTouchEvent方法,onTouch方法和onTouchEvent方法有何区别呢,它们的执行时机是什么?
带着这三个疑问,我踏上了google, 度娘,以及Android源代码探索的不归路~
Android中,所有的操作类型事件都由如下三个部分作为基础:
- 按下(ACTION_DOWN)
- 移动(ACTION_MOVE)
- 抬起(ACTION_UP)
这三部分都寄生于onTouch事件中,由MontionEvent类中定义的三个常量进行区分。
Android中与Touch事件相关的方法为:
Touch事件相关方法 |
方法功能 |
ViewGroup |
View(子View) |
Activity |
public boolean dispatchTouchEvnet(MotionEvent ev) |
事件分发 |
Yes |
Yes |
Yes |
public boolean onInterceptTouchEvent(MotionEvent ev) |
事件拦截 |
Yes |
No |
No |
public boolean onTouchEvent(MotionEvent ev) |
事件响应 |
Yes |
Yes |
Yes |
分发逻辑:整个Touch事件的分发其实是以Activity的dispatchTouchEvent作为起点,将事件传递给最外层ViewGroup的dispatchTouchEvent方法,再由该ViewGroup进行递归分发,直至叶子节点View的dispatchTouchEvent方法中。
为了证明这一逻辑,我们对代码一层层地分析,首先看下View中的dispatchTouchEvent方法:
View.Java
[java] view
plain copy
- 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;
- }
整个程序中有最关键的两处判断(第9行和14行),在第9行的if条件中,如果li.mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED 和 mOnTouchListener.onTouch(this, event) == true这三个条件同时满足时,就直接返回true。
第一个条件:mOnTouchListener在哪里赋值呢?CRTL+F搜索,发现了View.java中的如下方法:
View.java
[java] view
plain copy
- public void setOnTouchListener(OnTouchListener l) {
- getListenerInfo().mOnTouchListener = l;
- }
太眼熟了,这不正是我们平时注册onTouch事件所使用的函数么,这下明白了,这要注册了onTouch事件,mOnTouchListener就一定不为空。好了,继续往下看。
第二个条件:(mViewFlags & ENABLED_MASK) == ENABLED 判断当前的View是否为ENABLED,一般控件默认都是ENABLED,因此这个条件也成立。
第三个条件:mOnTouchListener.onTouch(this, event) == true,即我们注册的onTouch方法中,如果返回true,就会使三个条件都成立,这样,就不会继续接下来的判断了。
在第14行的If判断中,只是简单的判断了onTouchEvent方法的返回是否为true。
上述短短的几行代码解开了我的第三个疑问,下面来总结一下:
- 在View的dispatchTouchEvent方法中,最先执行的是onTouch方法,只有当onTouch方法返回false,才有机会去执行onTouchEvent方法。
- touch事件有层级传递机制,当我们为一个view注册touch事件,就会触发一系列的ACTION_DOWN,ACTION_MOVE_ACTION_UP,如果在执行ACTION_DOWN的时候返回false,则后面的ACTION_MOVE和ACTION_UP都得不到执行,即使用dispatchTouchEvent进行分发的时候,只有前一个action返回true,后一个action才能得到执行。
在上述的的第二个结论中,我们可知,只有当onTouch方法或者onTouchEvent都返回true,才能继续后面的操作,onTouch方法的返回一般是我们自己控制的,而onTouchEvent方法,若不自定义控件,则往往会使用它的默认实现(false),button是return true
- 关于Android中的事件机制,用到的地方还是很多的,并且这个知识点还真有点复杂。
在写这篇文章前,网上看了不少博文,有的写的感觉挺不错的。只是当时感觉好像理解了,事后又很容易忘。现在自己也系统整理下吧。
Android中的事件在表现形式上有很多,如onTouch、onClick和onLongClick等,在具体微观上的表现形势有action_down、action_move和action_up等。
无论哪种事件表现类型,首先都是基于事件的传递模型。其实Android中的事件传递有点类似于JS中事件传递模型。都是基于先捕获然后冒泡的形式。
在捕获阶段,事件先由外部的View接收,然后传递给其内层的View,依次传递到更够接收此事件的最小View单元,完成事件捕获过程;
在冒泡阶段,事件则从事件源的最小View单元开始,依次向外冒泡,将事件对层传递。
事件的捕获和冒泡是整个事件的传递流程,但是在实际的传递过程中,Android中则表现的相对复杂。
主要表现在可以控制每层事件是否继续传递(由事件分发和事件拦截协同进行),以及事件的具体消费(由事件消响应进行,但需要注意的是,事件分发自身也具有事件消费能力)。
也就是本文提及的事件分发、拦截和响应。
Android中不同的控件所具有的事件分发、拦截和响应稍有不同,主要表现在Activity本身不具有事件拦截,不是ViewGroup的最小view单元不具有事件拦截(因为它没有自己的子View)
- 事件分发:public boolean dispatchTouchEvent(MotionEvent ev)
当有监听到事件时,首先由Activity的捕获到,进入事件分发处理流程。无论是Activity还是View,如前文所说,事件分发自身也具有消费能力,
如果事件分发返回true,表示改事件在本层不再进行分发且已经在事件分发自身中被消费了。至此,事件已经完结。如果你不想Activity中的任何控件具有任何的事件消费能力,
最简答的方法可以重写此Activity的dispatchTouchEvent方法,直接返回true就ok(一开始就返回true)。
return false: 表明事件不会被进行分发。事件会以冒泡的方式被传递给上层的view或activity的onTouchEvent方法进行消费掉 。
当然了,如果本层控件已经是Activity(return true),那么事件将被系统消费或处理。
如果事件分发返回系统默认的 super.dispatchTouchEvent(ev),表明该事件将会被分发。此时当前View的onIntercepterTouchEvent方法会捕获该事件,判断需不需要进行事件的拦截
dispatchTouchEvent是处理触摸事件分发,事件(多数情况)是从Activity的dispatchTouchEvent开始的。执行
super.dispatchTouchEvent(ev),事件向下分发(如果本层控件是Activity,由于其没有事件拦截,因此将直接将事件传递到子View,并交给子View的事件分发进行处理)
在activity里面:
- public boolean dispatchTouchEvent(MotionEvent ev) {
- //如果是按下状态就调用onUserInteraction()方法,onUserInteraction()方法
- //是个空的方法, 我们直接跳过这里看下面的实现
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- onUserInteraction();
- }
- if (getWindow().superDispatchTouchEvent(ev)) {
- return true;
- }
- //getWindow().superDispatchTouchEvent(ev)返回false,这个事件就交给Activity
- //来处理, Activity的onTouchEvent()方法直接返回了false
- return onTouchEvent(ev);
从activity->PhoneWindow->DecorView
我们看到最顶层就是PhoneWindow$DecorView,接着DecorView下面有一个LinearLayout, LinearLayout下面有两个FrameLayout
上面那个FrameLayout是用来显示标题栏的,这个Demo中是一个TextView,当然我们还可以定制我们的标题栏,利用getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,R.layout.XXX); xxx就是我们自定义标题栏的布局XML文件
下面的FrameLayout是用来装载ContentView的,也就是我们在Activity中利用setContentView()方法设置的View,现在我们知道了,原来我们利用setContentView()设置Activity的View的外面还嵌套了这么多的东西
结论1:在activity里dispatchTouchEvent,如果走到子view返回true,getWindow().superDispatchTouchEvent(ev)才会返回true,activity才会返回true,接着后续的move
up才会往下走,如果getWindow().superDispatchTouchEvent(ev)返回false,导致Activity的onTouchEvent()方法直接返回了false,后续的move
up不会往下走,这也是为什么从最里层传回true,后续的move up才会往下走,如果一开始就把dispatchTouchEvent返回true,并没有往下走,activity就会消费onTouchEvent
事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
onInterceptTouchEvent是ViewGroup提供的方法,默认返回false
如果 onInterceptTouchEvent 返回 true,则表示将事件进行拦截,并将拦截到的事件交由本层控件 的 onTouchEvent 进行处理;
返回值:true
自己处理,不需要继续下传
事件会传递到自己的onTouchEvent()
Down事件在onInterceptTouchEvent()后返回true,则传递到onTouchEvent,
当其返回true时,动作序列的后续事件不会再通过onInterceptTouchEvent了,
而是在dispatchTouchEvent中直接传递于onTouchEvent
如果返回结果是false;则表示不对事件进行拦截,事件得以成功分发到子View。并由子View的dispatchTouchEvent进行处理。
返回值:false
自己无法完全处理,或者不能处理,继续下传
传递到下一个view的dispatchTouchEvent()
return super.inInterceptTouchEvent(ev):默认拦截方式,和return true一样。该事件会被拦截,将该事件交给当前view的onTouchEvent方法进行处理。(这里需要有一点说明,当有两个view。A
view中有一个B view.点击A.A中如果onInterceptTouchEvent()返回super.interceptTouchEvent(ev),则事件将会被A进行拦截,交给A的onTouchEvent()进行处理,如果点击的是B,A中如果onInterceptTouchEvent()返回super.interceptTouchEvent(ev),则事件将不会被拦截,会被分发到子控件中)
public boolean onTouchEvent(MotionEvent event)
return false:表明没有消费该事件,事件将会以冒泡的方式一直被传递到上层的view或Activity中的onTouchEvent事件处理。如果最上层的view或Activity中的onTouchEvent还是返回false。则该事件将消失。接下来来的一系列事件都将会直接被上层的onTouchEvent方法捕获
return true: 表明消费了该事件,事件到此结束。
return super.onTouchEvent(event):默认情况,和return false一样。
由于onInterceptTouchEvent()的机制比较复杂,上面的说明写的也比较复杂,总结一下,基本的规则是:
1. down事件首先会传递到onInterceptTouchEvent()方法
2. 如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return false,那么后续的move, up等事件将继续会先传递给该ViewGroup,之后才和down事件一样传递给最终的目标view的onTouchEvent()处理。
3. 如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return true,那么后续的move, up等事件将不再传递给onInterceptTouchEvent(),而是和down事件一样传递给该ViewGroup的onTouchEvent()处理,注意,目标view将接收不到任何事件。
4. 如果最终需要处理事件的view的onTouchEvent()返回了false,那么该事件将被传递至其上一层次的view的onTouchEvent()处理。
5. 如果最终需要处理事件的view 的onTouchEvent()返回了true,那么后续事件将可以继续传递给该view的onTouchEvent()处理。
验证:
MainActivity FatherView ChildView中几个方法都返回super.****TouchEvent(ev)
分析:
- 当点击屏幕。MainActivity 中的dispatchTouchEvent方法先执行,打印MainActivity-dispatchTouchEvent-->ACTION_DOWN
- 因为返回的是super.dispatchTouchEvent(ev),所以事件ev将会被分发,但是MainActivity中没有onInterceptTouchEvent()方法,所以事件被传递到FatherView中的dispatchTouchEvent方法.打印FatherView-dispatchTouchEvent-->ACTION_DOWN
- 在FatherView中dispatchTouchEvent返回的是super.dispatchTouchEvent(ev),所有事件会被分发。FatherView中的onInterceptTouchEven()中的方法被执行。打印FatherView-onInterceptTouchEven-->ACTION_DOWN
- FatherView中的onInterceptTouchEven()返回的是super.onInterceptTouchEvent(ev)。在这里,(1)如果点击的是屏幕中的ChildView。事件将不会被拦截,会被传递到ChildView中的dispatchTouchEvent方法中。(2)如果点击的值FatherView则事件将会被拦截。FatherView中的onTouchEvent()方法将被执行。以(1)为例,将打印ChildView-dispatchTouchEvent-->ACTION_DOWN。
- ChildView中dispatchTouchEvent返回的是super.dispatchTouchEvent(ev),所有事件会被分发。打印ChildView-onInterceptTouchEvent-->ACTION_DOWN。
- 此时ChildView中onInterceptTouchEvent返回的是super.onInterceptTouchEvent(ev),,而且已经没有子控件了,所以事件将被拦截。打印ChildView-onTouchEvent-->ACTION_DOWN。
- 在childView中onTouchEvent()返回额是super.onTouchEvent(ev)。事件将不会被消耗,将以冒泡的方式传递到上层空间中的onTouchEvent(),此处上层空间中的onTouchEvent返回的都是super.onTouchEvent(ev)。所以讲一次打印 Father-onTouchEvent-->ACTION_DOWN。 MainActivty-onTouchEvent-->ACTION_DOWN。
- 之后的事件动作,将不再被MainActivity分发到子view,直接被MainActivty中的onTouchEvent处理消耗。打印MainActivity-dispatchTouchEvent-->ACTION_UP,MainActivty-onTouchEvent-->ACTION_UP
MainActivity-dispatchTouchEvent-->ACTION_DOWN
FatherView-dispatchTouchEvent-->ACTION_DOWN
FatherView-onInterceptTouchEven-->ACTION_DOWN
ChildView-dispatchTouchEvent-->ACTION_DOWN
ChildView-onInterceptTouchEvent-->ACTION_DOWN。
ChildView-onTouchEvent-->ACTION_DOWN
Father-onTouchEvent-->ACTION_DOWN。
MainActivty-onTouchEvent-->ACTION_DOWN
MainActivity-dispatchTouchEvent-->ACTION_UP,
MainActivty-onTouchEvent-->ACTION_UP
Android中触摸事件传递过程中最重要的是dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()方法。这个是困扰初学者的问题之一,我开始也是。这里记录一下dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()的处理过程,以供记忆。
dispatchTouchEvent是处理触摸事件分发,事件(多数情况)是从Activity的dispatchTouchEvent开始的。执行
super.dispatchTouchEvent(ev),事件向下分发。
onInterceptTouchEvent是ViewGroup提供的方法,默认返回false,返回true表示拦截。
onTouchEvent是View中提供的方法,ViewGroup也有这个方法,view中不提供onInterceptTouchEvent。view中默认返回false
View里,有两个回调函数 :
[java] view
plain copy
- public boolean dispatchTouchEvent(MotionEvent ev);
- public boolean onTouchEvent(MotionEvent ev);
ViewGroup里,有三个回调函数 :
[java] view
plain copy
- public boolean dispatchTouchEvent(MotionEvent ev);
- public boolean onInterceptTouchEvent(MotionEvent ev);
- public boolean onTouchEvent(MotionEvent ev);
在Activity里,有两个回调函数 :
[java] view
plain copy
- public boolean dispatchTouchEvent(MotionEvent ev);
- public boolean onTouchEvent(MotionEvent ev);
Android中默认情况下事件传递是由最终的view的接收到,传递过程是从父布局到子布局,也就是从Activity到ViewGroup到View的过程,默认情况,ViewGroup起到的是透传作用。Android中事件传递过程(按箭头方向)如下图,图片来自[qiushuiqifei],谢谢[qiushuiqifei]整理。
触摸事件是一连串ACTION_DOWN,ACTION_MOVE..MOVE…MOVE、最后ACTION_UP,触摸事件还有ACTION_CANCEL事件。事件都是从ACTION_DOWN开始的,Activity的dispatchTouchEvent()首先接收到ACTION_DOWN,执行super.dispatchTouchEvent(ev),事件向下分发。
dispatchTouchEvent()返回true,后续事件(ACTION_MOVE、ACTION_UP)会再传递,如果返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。
下面的几张图参考自[eoe]
图1.ACTION_DOWN都没被消费
图2-1.ACTION_DOWN被View消费了
图2-2.后续ACTION_MOVE和UP在不被拦截的情况下都会去找VIEW
图3.后续的被拦截了
图4ACTION_DOWN一开始就被拦截
android中的Touch事件都是从ACTION_DOWN开始的:
单手指操作:ACTION_DOWN---ACTION_MOVE----ACTION_UP
多手指操作:ACTION_DOWN---ACTION_POINTER_DOWN---ACTION_MOVE--ACTION_POINTER_UP---ACTION_UP.