-文章来源:itsCoder 的 WeeklyBolg 项目
- itsCoder主页:http://itscoder.com/
- 作者:yongyu0102
- 审阅者:HanJie
前言
笔记内容源于Android 开发艺术探索。
在写这篇笔记的时候想了好久,也拖了好长时间,关于事件分发的博客看了很多,有的写的思路很清晰,画了事件分发的整体流程图,但是没有源码,看过之后只能知道事件是怎么分发的,但完全是记住的,而不是通过源码分析出来的,试想,如果以后再遇到其他知识点还是这样,那么我们就完全成了不能靠自己去分析问题,只能去食他人知识,没有自我学习分析能力,所以笔者试着结合艺术探索的讲解,尝试在源码的基础上加以理解,本文的写作逻辑是先从文字描述上尽量让大家先大概了解,事件分发的概况,先有个感性认识,再结合源码进行分析,如果错误的地方,还请指出。
1.1 点击事件的传递规则
在我们进行分析事件分发机制之前,先思考一下我们要研究哪些问题:
1、事件分发的对象是什么?
当用户触摸屏幕时,将创建一个 MotionEvent 对象即点击事件。MotionEvent 包含关于发生触摸的位置、时间、历史记录、手势动作等细节信息, Touch 事件相关细节被封装成了 MotionEvent 对象。理解了这一个知识点后,其实我们就很容易理解所谓点击事件的分发,其实就是对 MotionEvent 事件分发的过程,即当一个 MotionEvent 产生后,系统需要把这个事件传递给一个具体的 View 去处理, 而这个传递的过程就是分发过程。
2、事件是在哪些对象之间传递?
Android 与用户交互的界面就是由一系列 Activity(Fragment)、ViewGroup、View 组成如图:
当然一个界面可能由多个ViewGroup或者多个View 组成,所以事件就是在这三者之间进行传递,我们要分析的就是要捋清楚事件是由哪个对象发出,经过哪些对象,最终达到哪个对象,在某些条件改变的时候下他们之间的关系又是怎样的,理解了这些之后,当我们在遇到点击或者滑动事件冲突的情况,相信一切问题就迎刃而解了。
3、这个分发过程由哪些对象协作完成?
其实点击事件分发过程主要由三个重要方法共同完成:dispatchTouchEvent(MotionEvent event) 、onInterceptTouchEvent(MotionEvent event)和onTouchEvent(MotionEvent event),下面先介绍一下这三个方法的主要作用,这样有利于后面我们对源码的分析:
public boolean dispatchTouchEvent(MotionEvent event) :用来进行事件的分发。如果事件能够传递给当前view,那么此方法一定会被调用,返回结果受当前 view 的 onTouchEvent 和下级 view 的 dispatchTouchEvent 方法的影响,表示是否消费当前事件。返回值 true,表示触摸事件被消费(注意:这里事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关),已经分发出去,后续事件会继续分发到该 View;返回值 false,则表示触摸事件没有被消费,即事件没有分发出去,那么后续事件就不会继续向该 View 分发,该方法在 View 和 ViewGroup 中都有。
public boolean onInterceptTouchEvent(MotionEvent event)
在 dispatchTouchEvent 方法内部调用,用来判断是否拦截某个事件,如果当前 view 拦截了某个事件,那么在同一个事件序列当中,此方法不会再被调用,返回结果表示是否拦截当前事件。只有 ViewGroup 中才有该方法 ,返回值 true,表示ViewGroup拦截了该触摸事件,该事件就不会分发给它的子 View 或者子 ViewGroup,事件会由自己的 onTouchEvent() 方法处理。返回值 false,表示 ViewGroup 没有拦截该事件,该事件就可以分发给它的子 View 和子 ViewGroup ,事件传递到子 view 的 dispatchTouchEvent() 方法中去处理。public boolean onTouchEvent(MotionEvent event)
在 dispatchTouchEvent 方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果 ACTION_DOWN不消耗,则在同一个事件序列中,当前 view 无法再次接收到事件。返回值为 True ,事件由自己处理,后续事件序列让其处理;返回值为 False ,自己不消耗事件,向上返回让其他的父容器的onTouchEvent接受处理。这三个方法都是通过 dispatchTouchEvent() 联系在一起,他们之间的关系可以用如下伪代码表示:
123456789101112131415public boolean dispatchTouchEvent(MotionEvent ev) {//表示事件是否被分发出去(消耗),默认为false,即默认不拦截boolean consume = false;if (onInterceptTouchEvent(ev)) {//如果拦截了,那么事件交给当前view的onTouchEvent(ev)//方法进行处理,返回值即onTouchEvent(ev)的结果consume = onTouchEvent(ev);} else {//如果不拦截,那么事件交给子view的dispatchTouchEvent(ev)//方法进行处理,返回值即是dispatchTouchEvent(ev)事件//处理结果consume = child.dispatchTouchEvent(ev);}return consume;}
结论:对于一个根 ViewGroup,点击事件产生后,首先会传递给它,这时 ViewGroup 的 dispatchTouchEvent 会调用,而在 dispatchTouchEvent 方法中会调用 onInterceptTouchEvent() 方法,如果它的 onInterceptTouchEvent 返回 true 表示要拦截当前事件,接下来事件会交给这个 ViewGroup 处理,此时 ViewGroup 的 onTouchEvent 方法就会被调用,如果这个ViewGroup 的 onInterceptTouchEvent 返回 false,则事件会继续传递给子元素,子元素的 dispatchTouchEvent 会调用,如此反复直到事件被处理。
当一个View需要处理事件时,如果设置了 OnTouchListener ,那么 OnTouchListener 的 onTouch方法会回调,如果 onTouch 返回 false,则当前 View 的 onTouchEvent 方法会被调用;如果返回 true,那么 onTouchEvent 方法将不会调用。由此可见,OnTouchListener 优先级高于 onTouchEvent。OnClickListener 优先级处在事件传递的尾端。
4、事件的传递顺序是什么?
这里就直接给出答案:一个点击事件产生后,传递顺序:Activity -> Window -> ViewGroup -> View;如果一个 View 的 onTouchEvent 返回 false 即 View 没有处理事件,那么它的父容器的onTouchEvent 会被调用,以此类推,所有元素都不处理该事件,最终将传递给 Activity 处理,即 Activity 的 onTouchEvent 会被调用,事件顺序为:View -> ViewGroup -> Window -> Activity。
5、关于事件传递的其他一些结论这里先给出,以便我们更好的理解时间传递机制,结论如下:
- 同一个事件序列是指从手指触摸屏幕那一刻开始,中间包含数量不定的 move 事件到手指离开屏幕那一刻(down->move…move->up)。
- 正常情况下一个事件序列只能被一个 View 拦截且消耗,每个 View 一旦决定拦截,同一个事件序列所有事件都会直接交给它处理,并且它的 onInterceptTouchEvent 不会再被调用。
- 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN( onTouchEvent 返回了 false ),那么同一事件序列中其他事件都不会再交给它来处理,事件将重新交给他的父元素处理,即父元素的 onTouchEvent 会被调用。
- 如果某个 View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以收到后续事件,最终这些消失的点击事件会传递给 Activity 处理。
- ViewGroup 默认不拦截任何事件,ViewGroup 的 onInterceptTouchEvent 方法默认返回 false。
- View 没有 onInterceptTouchEvent 方法,一旦有事件传递给它,那么它的 onTouchEvent 方法就会被调用。
- View 的 onTouchEvent 方法默认消耗事件(返回 true ),除非他是不可点击的( clickable 和 longClickable 同时为 false )。View 的 longClickable 属性默认都为 false ,clickable 属性分情况,Button 默认为 true,TextView 默认为 false。
- onClick 发生的前提是View可点击,并且它收到了down 和 up 事件。
- 事件传递过程是由外而内,事件总是先传递给父元素,然后在由父元素分发给子 View,通过requestDisallowInterceptTouchEvent 方法可以在子元素干预父元素的事件分发过程,但 ACTION_DOWN 事件除外。
1.2 事件分发的源码解析
这里要分别分析 Activity 、ViewGroup 和 View 对事件的分发。
1. Activity 对点击事件的分发过程
由于平时我们对 Activity 事件分发接触不是很多(笔者是这样),使用应该不是很多,所以这里只做简单介绍。
(1) Activity 中与触摸事件相关API主要是 dispatchTouchEvent() 和 onTouchEvent()。dispatchTouchEvent() 是传递触摸事件的API,而 onTouchEvent() 则是 Activity 处理触摸事件的API。
(2) Activity 中的 dispatchTouchEven 会将触摸事件传递给Activity 所包含的视图。具体的实现方式在通过调用到 Activity 所属 Window 的 superDispatchTouchEvent,进而调用到 Window 的 DecorView 的 superDispatchTouchEvent,进一步又调用到 ViewGroup 的 dispatchTouchEvent() ,这样事件就从 Activity 传递到了 ViewGroup 。
如果 Activity 所包含的视图拦截或者消费了该触摸事件的话,就不会再执行 Activity 的 onTouchEvent() ;
如果 Activity 所包含的视图没有拦截或者消费该触摸事件的话,则会执行 Activity 的 onTouchEvent() 。
(3) Activity 中的 onTouchEvent 是 Activity 自身对触摸事件的处理。如果该 Activity 的 android:windowCloseOnTouchOutside 属性为 true,并且当前触摸事件是 ACTION_DOWN ,而且该触摸事件的坐标在 Activity 之外,同时 Activity 还包含了视图的话就会导致 Activity 被结束。
2. View 对点击事件的处理过程
这里的 View 不包含 ViewGroup ,这里说下为什么先分析 View 而不是像书上那样先分析 ViewGroup ,因为在 ViewGroup 的事件分发过程中会调用到 View 对事件的处理,而且 View 的事件处理相比 ViewGroup 而言简单些,所以这里先分析 View 。
我们来看一下 View 中 dispatchTouchEvent 方法的源码:
|
|
从上面源码的10行代码可以看出,首先会判断 mOnTouchListener 是否为空,而mOnTouchListener 是在 setOnTouchListener 方法里赋值的,也就是说只要我们给控件注册了touch 事件,mOnTouchListener 就一定不为空,而(mViewFlags & ENABLED_MASK) == ENABLED 是判断当前点击的控件是否是 enable 的,按钮默认都是 enable 的,因此这个条件恒定为 true。mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册 touch 事件时的 onTouch 方法。也就是说如果我们在 onTouch 方法里返回 true,就会让这三个条件全部成立,从而整个方法返回 true。在结合22行代码 if (!result && onTouchEvent(event)),可以得出结论,如果我们在 onTouch 方法里返回 false,就会再去执行 onTouchEvent(event)方法,如果 onTouch 方法里返回 true ,onTouchEvent(event) 方法将不会执行, 可见 OnTouchListener 优先级高于 onTouchEvent(event) 方法。
接下来我们分析一下 onTouchEvent(event) 方法的源码:
|
|
先看第7行代码 if ((viewFlags & ENABLED_MASK) == DISABLED) 这个条件是判断 view 是不可用状态,而在该条件下我们看第17行代码 如下:
|
|
这个返回结果是判断只要 view 是可点击状态 那么返回值就问 true ,即如果 view 是不可用状态,但只要 view 是可点击状态,就会消耗点击事件,只不过不响应结果。
再看第33行会判断只要 view 是可点击的,那么就会进入到下面的 switch 语句,最终在第123行返回 true,即只要 view 是可点击的,那么 ontouchEvent(Event v) 方法就会返回 true 消费点击事件。
然后在 ACTION_UP 事件触发的时候,在55行代码会执行 performClick() 方法,我们看一下这个方法的实现:
|
|
上面第6行代码 if (li != null && li.mOnClickListener != null) 判断如果给当前 view 设置了点击事件,那么就会执行回调设置的 onClick() 方法即 mOnClickListener.onClick(this),这样 view 的 dispatchTouchEvent(MotionEvent event) 这里就分析结束了!
3 ViewGroup 对点击事件的处理过程
同样我们来看一下 ViewGroup 中 dispatchTouchEvent 方法的源码:
|
|
事件分发主要分为以上几个步骤,代码中已经标出,下面加以总结:
在上面执行第2步的时候,ViewGroup 在两种情况下会判断是否拦截事件即 事件类型为 ACTION_DOWN 和 mFirstTouchTarget != null 条件下, ACTION_DOWN 好理解,那么 mFirstTouchTarget != null 是什么意思呢,我们看一下在执行第5步,事件由 ViewGroup 子 View 处理成功的时候有一行代码是:newTouchTarget = addTouchTarget(child, idBitsToAssign);这行代码实现方法如下:
|
|
通过上面代码可以看出,事件由 ViewGroup 子 View 处理成功的时候 mFirstTouchTarget 将会被赋值,即事件分发到子 view 的时候mFirstTouchTarget != null,反过来如果事件由 ViewGroup 进行拦截,那么mFirstTouchTarget != null 就不成立,那么当 ACTION_MOVE 和 ACIONT_UP 事件到来的时候 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 条件为 false,将导致 ViewGroup 的 onInterceptTouchEvent(ev) 方法不再调用,同一系列事件中的其他事件将由 ViewGroup 进行处理。当然有一种特殊情况,就是 FLAG_DISALLOW_INTERCEPT 这个标记位,这个标记位是子 view 调用了 requestDisallowInterceptTouchEvent() 来进行设置,ViewGroup 将无法进行拦截除了 ACTION_DOWN 以外的其他事件,这是因为在进行 ACTION_DOWN 的时候,会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,这样导致子 view 设置的 requestDisallowInterceptTouchEvent() 失效。
在看一下第5步,是如何调用 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) 方法进行事件分发的,这行代码实现方法如下:
|
|
我们只看与我们要分析目标相关的代码,上面代码中我们看到,如果 child!=null ,那么执行 handled = child.dispatchTouchEvent(event) 之后的逻辑就是前面分析的 view 的事件分发,这样就完成了事件分发到子 view 的流程,我们看 child.dispatchTouchEvent(event) 是由返回值的,即如果返回 true ,事件由子 view 消费,事件分发成功,跳出第4步 for 循环,停止遍历子 view。
我们再看一下第7步,如果 mFirstTouchTarget == null ,说明没有任何子 view 接受触摸事件,那么调用 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS) 注意这里第三个参数传入 null,就是我们上面分析的另一种情况 ,如果 child == null ,执行 handled = super.dispatchTouchEvent(event) ,而 ViewGroup 的父类是 view ,所以之后的处理逻辑又和前面说的 view 一样了,即 ViewGroup 的 onTouch 方法会得到执行 ,而如果 mFirstTouchTarget != null ,表明已经有事件分发成功了,那么会执行 while (target != null) 循环从从链表 mFirstTouchTarget 中取出 target,条件 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) 如果成立,那么说明这个事件是是已经分发过成功的事件,那么直接执行 handled = true ,即让 dispatchTouchEvent 方法返回 true, 表明事件分发成功,而如果 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) 不成立,表明该事件是新到来的事件还没进行分发,那么执行 dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) ,进一步对事件进行分发,并将 分发处理结果进行返回,这这情况下就是前面 第3步所说,不执行第三步,直接执行第7步对事件进一步就行分发。
最后说了这么多,大家可以结合下面这张流程图来对整理分发流程进行梳理,图片引自:
其中 super 表示调用关系,true 表示消费事件, false 表示没有消费事件。
到这里关于 View 的事件分发分析就结束了!
如果感觉笔者写的不好,可以参考以下博客: