文章中出现的源码均基于9.0
一、 基本认识
1. 事件分发的本质
将点击事件(MotionEvent)传递到某个具体的View处理的整个过程
2. 事件分发的对象
点击事件(Touch事件):当用户触摸屏幕时,将产生点击事件
事件类型:
类型 | 说明 |
---|---|
MotionEvent.ACTION_DOWN | 手指刚接触屏幕,一般为事件的开始 |
MotionEvent.ACTION_MOVE | 手指在屏幕移动,在移动的过程中会产生多个move事件 |
MotionEvent.ACTION_UP | 手指从屏幕上松开的一瞬间 |
MotionEvent.ACTION_CANCEL | 结束事件,非人为原因 |
同一个事件序列:指从手指刚接触屏幕,到手指离开屏幕的那一刻结束,在这一过程产生的一系列事件,这个序列一般以down事件开始,中间含有多个move事件,最终以up事件结束
3. 事件分发的顺序
事件传递的顺序:Activity->Window->DecorView->ViewGroup->View。一个点击事件发生后,总是先传递给当前的Activity,然后通过Window传给DecorView再传给ViewGroup,最终传到View。
在《开发艺术探索》中的事件分发的顺序是:Activity->Window->View,而有的博客上的顺序是:Activity->ViewGroup->View,不过其实两者是一样的(下列会从源码进行分析)。Window是抽象类,其唯一实现类为PhoneWindow,PhoneWindow将事件直接传递给DecorView,而DecorView继承FrameLayout,FrameLayout又是ViewGroup的子类,所以兜兜转转最后也可以认为Window事件分发的实现其实是ViewGroup来实现的。所以也可以认为事件传递的顺序是:Activity->ViewGroup->View。
二、核心方法
View的事件分发机制主要由事件分发->事件拦截->事件响应三步来进行逻辑控制,很巧的这三步刚好对应了三个核心方法
1. 事件分发:dispatchTouchEvent
用来进行事件的分发,如果事件能够传递给当前View,则该方法一定会被调用。返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent的影响,表示是否消耗当前事件。
原型:public boolean dispatchTouchEvent(MotionEvent ev)
return:
- ture:当前View消耗所有事件
- false:停止分发,交由上层控件的onTouchEvent方法进行消费,如果本层控件是Activity,则事件将被系统消费,处理
2. 事件拦截:onInterceptTouchEvent
需注意的是在Activity,ViewGroup,View中只有ViewGroup有这个方法。故一旦有点击事件传递给View,则View的onTouchEvent方法就会被调用
在dispatchTouch Event内部使用,用来判断是否拦截事件。如果当前View拦截了某个事件,那么该事件序列的其它方法也由当前View处理,故该方法不会被再次调用,因为已经无须询问它是否要拦截该事件。
原型:public boolean onInterceptTouchEvent(MotionEvent ev)
return:
- ture:对事件拦截,交给本层的onTouchEvent进行处理
- false:不拦截,分发到子View,由子View的dispatchTouchEvent进行处理
- super.onInterceptTouchEvent(ev):默认不拦截
3. 事件响应:onTouchEvent
在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前View无法再接受到剩下的事件,并且事件将重新交给它的父元素处理,即父元素的onTouchEvent会被调用
原型:public boolean onTouchEvent(MotionEvent ev)
return:
- true:表示onTouchEvent处理后消耗了当前事件
- false:不响应事件,不断的传递给上层的onTouchEvent方法处理,直到某个View的onTouchEvent返回true,则认为该事件被消费,如果到最顶层View还是返回false,则该事件不消费,将交由Activity的onTouchEvent处理。
- super.onTouchEvent(ev):默认消耗当前事件,与返回true一致。
三、事件分发机制
在分析事件分发机制时,应该从事件分发的顺序入手一步一步解剖。从上文我们知道事件分发顺序为:Activity->Window->DecorView->ViewGroup->View。由于Window与DecorView可以看作是Activity->ViewGroup的过程,故这里将从三部分通过源码来分析事件分发机制:
- Activity对点击事件的分发机制
- ViewGroup对点击事件的分发机制
- View对点击事件的分发机制
1. Activity事件的分发机制
我们知道,当一个点击事件发生时,事件总是最先传递到当前Activity中,由Activity的dispatchTouchEvent来进行事件分发。而Activity会将事件传递给Window对象来分发,Window对象再传递给DecorView。下面将进行源码分析来验证这个过程:
源码:Activity#dispatchTouchEvent
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
上面代码为Activity中的dispatchTouchEvent的源码,通过源码我们可以知道当点击事件发生时,首先会执行onUserInteraction();这个方法又是什么呢?不急,我们跟踪下去。
1 | //空方法,当该Activity在栈顶时,触屏点击home,back,menu会触发此方法 |
从源码中可以看出在Activity中该方法为空方法,当该Activity在栈顶时,触屏点击home,back,menu会触发此方法,所以这个方法可以实现屏保功能。让我们回到Activity中的dispatchTouchEvent方法中,接着调用了getWindow().superDispatchTouchEvent(ev)方法将事件交给Activity所附属的Window进行分发,如果最终事件被消耗了,则返回true,如果事件没人处理,则Activity调用在自己的onTouch()方法来处理事件。
getWindow是一个Window对象,在Window源码中我们可以发现其实Window就是一个抽象类,显而易见其方法自然是抽象方法,所以我们必须找出其具体实现类。
源码:Window#superDispatchTouchEvent
1 | /** |
从源码中我们可以发现在Window类前面的注释中是有解释的,这时候就要考验偶们的英语能力辽!其实总体上来说还是挺容易理解的,其实我们只要关注后面一部分的注释就行。
The only existing implementation of this abstract class isandroid.view.PhoneWindow, which you should instantiate when needing a Window.
从这里我们可以知道它的唯一实现类就是PhoneWindow,废话不多说,我们直接看看PhoneWindow中superDispatchTouchEvent方法的实现是如何的呢?
源码:PhoneWindow#superDispatchTouchEvent
1 | private DecorView mDecor; |
从源码中可以发现PhoneWindow将事件直接传递给了DecorView,而这个DecorView又是何方圣神呢?如下所示
1 | public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks { |
其实DecorView就是我们通过setContentView设置布局的父容器,我们可以通过getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)这个方式就能获取到setContentView中设置的布局。从DecorView的源码中可以发现DecorView是继承FrameLayout,而FrameLayout又是继承ViewGroup的,故DecorView的间接父类为ViewGroup,在DecorView中superDispatchTouchEvent方法是使用super来调用父类的dispatchTouchEvent,故等于调用ViewGroup的dispatchTouchEvent方法(从源码中我们可以得知FrameLayout并没有dispatchTouchEvent这个方法),于是DecorView将事件传递到了ViewGroup去处理。也可以这么说,事件已经传递到了顶级View也就是Activity中通过setContentView所设置的View(顶级View通常为ViewGroup)。
流程图如下:
到这里,我们也验证了前面提到的事件分发的顺序是:Activity->Window->DecorView->ViewGroup。那么ViewGroup又是如何将事件传递给View呢?让我们来继续分析!
2. ViewGroup事件的分发机制
从上面Activity事件的分发机制我们可以知道,ViewGroup事件分发机制是从dispatchTouchEvent()开始的,所以我们从这部分的源码开始分析,由于该方法代码量很多,下面将根据需要贴出相关代码:
源码:ViewGroup#dispatchTouchEvent
1 |
|
从上面源码我们可以知道,ViewGroup判断是否要拦截只会是在ACTION_DOWN的时候,或者是mFirstTouchTarget != null。mFirstTouchTarget 从后面的代码才能知道其作用。它的作用就是:当事件被ViewGroup的某个子View处理时,mFirstTouchTarget 就会指向这个子View。所以当事件被这个ViewGroup拦截时,mFirstTouchTarget =null,那么这个时候ACTION_MOVE和ACTION_UP事件到来时,由于判断条件为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,然后intercepted被赋予true,所以同一事件序列的其它事件都会默认交给该ViewGroup来处理。在上面源码中我们还可以发现这么一句语句:
1 | final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; |
FLAG_DISALLOW_INTERCEPT是个标记位,这个标记位是通过 requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法来设置的,一般用在子View中。如果FLAG_DISALLOW_INTERCEPT被设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其它点击事件,这是因为ViewGroup在分发事件中,如果是ACTION_DOWN事件,将会重置FLAG_DISALLOW_INTERCEPT这个标记位。让我们来看看源码。
1 | if (actionMasked == MotionEvent.ACTION_DOWN) { |
上面源码是在判断是否拦截的前面的,所以能够重置标记位,从这里我们也可以发现,当点击事件为ACTION_DOWN时,ViewGroup总是会调用自己的onInterceptTouchEvent来询问自己是否要拦截事件。
requestDisallowInterceptTouchEvent方法针对的是ACTION_DOWN以外的其他事件,并且是在不拦截ACTION_DOWN事件的情况下才会起作用。
接下来让我们瞧瞧ViewGroup不再拦截事件的时候,事件的分发情况,源码如下:
1 | final View[] children = mChildren; |
从上面代码中我们可以知道,不拦截事件时,首先会遍历ViewGroup的所有子元素,然后判断子元素是否能够接受到点击事件。判断的依据是:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果找到一个目标子View来处理事件时,则调用dispatchTransformedTouchEvent()方法。来看看这个方法重要实现逻辑:
1 | if (child == null) { |
可以发现由于在上面中的child并不等于null,所以将直接调用子元素的dispatchTouchEvent方法,使得事件传递到子View上,然后继续分发。
你以为这样就结束了?答案肯定是没有,从上面源码中我们可以发现当子元素的dispatchTouchEvent返回true后,还有相应操作:
1 | newTouchTarget = addTouchTarget(child, idBitsToAssign); |
这几行代码完成了mFirstTouchTarget的赋值并终止了对子元素的遍历。如果子元素的dispatchTouchEvent返回false,则ViewGroup就会把事件分发给下一个元素(如果还有子元素的话),看到这你也许又纳闷了,mFirstTouchTarget的赋值?怎么没看见mFirstTouchTarget的影子呢,答案其实在addTouchTarget这个方法中:
1 | private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { |
这个方法可以看出其实mFirstTouchTarget是一种单链表结构,首先根据坐标点找到了目标子View,然后将子View放在链表头上,从而实现了mFirstTouchTarget!=null。
到这里就完成了ViewGroup一轮的事件分发了,然而还没有结束,如果遍历了所有子元素后事件都没有被合适处理呢?
没有合适处理包括了两种情况:
- ViewGroup没有子元素
- 子元素处理了点击事件,但是在dispatchTouchEvent中返回了false(默认是返回true,只有重写View的这个方法或者在onTouchEvent中返回了false)
那么这时候ViewGroup将会自己处理点击事件(当ViewGroup拦截了事件时也是做同样的处理)。
1 | if (mFirstTouchTarget == null) { |
可以看出这时候还是调用了dispatchTransformedTouchEvent方法,不过这时候第三个参数不是child而是null,所以会调用下面这句代码:
1 | //dispatchTransformedTouchEvent |
super其实就是View中的dispatchTouchEvent方法,所以点击事件开始交由View来处理。
ViewGroup并没有调用onTouchEvent,ViewGroup也没有去重写onTouchEvent
流程图如下:
3. View事件的分发机制
从上面对ViewGroup事件分发机制可知,View事件分发机制是从dispatchTouchEvent开始的。
源码:View#dispatchTouchEvent
1 | public boolean dispatchTouchEvent(MotionEvent event) { |
由于View是一个单独元素,没有子元素可以继续向下传递事件,只能自己处理事件,所以代码也会明显减少。从上面的源码中我们可以看到View对点击事件的处理过程,result代表是否消耗该事件,然后进行onTouchListener的判断,如果onTouchListenter中的onTouch方法返回了true,那么就不会再调用onTouchEvent方法,由此可见onTouchListener的优先级高于onTouchEvent。
然后来看看onTouchEvent的实现。
1 | public boolean onTouchEvent(MotionEvent event) { |
从上面代码可以知道只要View的CLICKABLE,LONG_CLICKABLE,CONTEXT_CLICKABLE有一个为true,那么它就会消耗该事件,不管它是不是DISABLE状态。然后假如控件可点击,就对四种事件类型进行相对应的处理,这里值得一说的是ACTION_UP事件,从源码中可以发现在ACTION_UP事件发生时,会触发performClickInternal方法。这个方法内部实现又是怎样的呢?如下:
1 | private boolean performClickInternal() { |
我们可以发现最后还是会调用performClick,而在performClick内部中:
1 | public boolean performClick() { |
只要我们通过setOnClickListener为View注册点击事件,那么就会给li.mOnClickListener赋值,则会调用onClick方法。
流程图如下:
从流程图我们可以发现onTouch,onTouchEvent,onClick的优先级:onTouch>onTouchEvent>onClick
到这里,我们已经通过源码将点击事件的分发机制梳理一遍了。