
笔记:事件分发机制(二):ViewGroup的事件分发
前言
做一下笔记。从源码角度深入分析和理解一下ViewGroup的事件分发。
ViewGroup
ViewGroup是View 的子类,一般作为容器,盛放其他View和ViewGroup。是Android布局控件的直接或间接父类,像LinearLayout、FrameLayout、RelativeLayout等都属于ViewGroup的子类。ViewGroup与View相比,多了可以包含子View和定义布局参数的功能。ViewGroup的继承关系如下:
从ViewGroup的子类中可以找到平常经常用到的布局控件。
ViewGroup的事件分发流程
demo
简单了解了一下ViewGroup,接下来用demo探索ViewGroup的事件分发流程。
首先,自定义一个布局命名为MyLayout继承自LinearLayout,如下所示:
然后打开布局文件 activity_view_group.xml ,用MyLayout作为根布局,设置属性,添加控件。如下:
在MyLayout布局中添加两个按钮,接着在Activity中为MyLayout和两个按钮添加监听事件:
设置好监听事件,运行一下:
分别点击Button1、Button2和空白区域,打印结果如下:
根据log打印可以看出,当点击按钮时,MyLayout注册的触摸监听(OnTouchEvent)的onTouch方法并没有执行,只有点击空白区域时才会执行onTouch方法。此时,可以先理解为Button的onClick方法将事件消费掉了,因此事件不再向下传递。
那么事件的传递流程到底是怎样的呢?难道是事件先经过子View再到ViewGroup吗?欲知答案,继续跟进…
源码分析
onInterceptTouchEvent事件拦截
在事件分发机制中,ViewGroup有一个View不具备的方法–onInterceptTouchEvent
源码注释很多,代码很简单,返回值是boolean类型,既然如此,可以重写这个方法,返回一个true试试。
再次运行,分别点击Button1、Button2和空白区域,log打印如下:
发现不管在哪里点击,只会触发MyLayout的touch事件,按钮的点击事件被屏蔽了!这是为什么?如果touch事件先传递到子控件后传递到ViewGroup,那么MyLayout怎么可能屏蔽掉Button的点击事件呢?明明之前点击MyLayout中的Button,只触发了按钮的点击事件,没有触发MyLayout的touch事件。
只能从源码中找答案,才能解决心中的疑惑。事先说明,Android中的touch事件的传递,绝对是先传递到ViewGroup,后传递到View的。
ViewGroup的事件分发dispatchTouchEvent
记得在上一篇(笔记:事件分发机制(一):View的事件分发)中分析过,当触摸一个控件,都会先调用该控件的dispatchTouchEvent()方法。说法没错,但并不完整。实际情况是当你触摸某个控件,首先调用该控件所在布局的dispatchTouchEvent()方法,然后在布局的dispatchTouchEvent()方法中找到相应的被触摸控件,然后在调用该空间的dispatchTouchEvent()方法。所以,当我们点击了MyLayout的按钮,首先会调用MyLayout的dispatchTouchEvent()方法,这时会发现MyLayout和它的父类LinearLayout中没有这个方法,继续往上找在ViewGroup中发现了dispatchTouchEvent()方法。
既然在ViewGroup找到了dispatchTouchEvent()方法,那么就看一下这里的dispatchTouchEvent()方法:
从上面的源码可知,当ViewGroup不作事件拦截,会遍历ViewGroup的所有子元素,然后判断子元素是否能够接收到事件。是否能够接收到事件主要由两点判断:子元素是否在播放动画和点击事件是否落在子元素的边界范围内。如果某个子元素满足这两个条件,那么事件就会传递给这个子控件来处理。可以看到,dispatchTransformedTouchEvent实际上调用的是子元素的dispatchEvent方法,其内部有一段代码:
如果找到了能够接收事件的子View,就会由这个View分发处理事件,否则将当前ViewGroup作为View(ViewGroup的super是View)分发处理该事件。
如果子元素的dispatchTouchEvent返回true,我们就暂时不用考虑事件是怎样在子View中分发的。那么mFirstTouchTarget就会被赋值同时跳出for循环,如下所示:
这几行代码完成了mFirstTouchTarget的赋值并终止对子元素的遍历。如果子元素的dispatchTouchEvent返回false,ViewGroup会继续遍历,把事件分发给下一个符合条件的子元素(如果有的话)。
其实mFirstTouchTarget的赋值是在addTouchTarget方法中,从下面addTouchTarget方法的源码中可以看出,mFirstTouchTarget其实是一种单链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来的同一序列中(down-up、down-move…-up等)的所有触摸事件。
如果遍历了所有子元素后,事件没有被合理地处理。这包含两种情况:一,ViewGroup没有子元素;二,子元素处理了事件,但在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。在这两种情况下,ViewGroup会自己处理点击事件。如下:
注意dispatchTransformedTouchEvent的第三个参数child为null,从dispatchTransformedTouchEvent方法的分析可知,它会调用super.dispatchTouchEvent(event);,而这里的super是一个View(ViewGroup的super是View),也就是ViewGroup作为View去处理事件,这就转到了View的事件分发。
下面是dispatchTransformedTouchEvent的源码:
Activity对事件的分发过程
点击事件用MotionEvent来表示,当一个点击操作发生时,事件首先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件分发,具体工作是由Activity内部的Window来完成的。Window会将事件传递给DecorView,DecorView一般是当前界面的底层容器(也就是setContentView所设置View的父容器),通过activity.getWindow().getDecorView()可以获得。先从Activity的dispatchTouchEvent开始分析:
分析一下源码,发现事件首先被交给Activity所附属的Window进行分发,如果返回true,整个事件分发过程结束,如果返回false,意味着事件没有控件处理,那么Activity的onTouchEvent就会被调用。
接下来看Window是如何将事件传递给ViewGroup的。通过源码我们知道,Window是一个抽象类,而Window的superDispatchTouchEvent是个抽象方法,因此必须找到Window的实现类才行。
Window的实现类到底是谁?其实是PhoneWindow,这一点从Window的源码可以知道,在Window源码的类注释中有这么一段话:
大概意思是:Window类可以控制顶层View的外观和行为策略。它的唯一实现是android.policy.PhoneWindow,当你想要实例化这个Window类时,你并不 知道它的细节,因为这个类会被重构,只有一个工厂方法可以使用。
由于Window的唯一实现是PhoneWindow,那么就看一下PhoneWindow是怎样处理事件的。也就是看一下superDispatchTouchEvent方法:
到这里就很清楚了,PhoneWindow将事件传递给了DecorView,这个DecorView是什么呢?看下面:
我们可以通过getWindow().getDecorView().findViewById(R.id.content).getChildAt(0)获取Activity通过所设置的View。这个mDecor就是getWindow().getDecorView()返回的View,而我们通过setContentView设置的View是它的一个子View。现在事件传递到了DecorView这里,由于DecorView继承自FrameLayout,且是父View,所以最终事件会传递给setContentView设置View。从这里开始,事件就传递到顶级View了,也就是Activity中通过setContentView设置View,另外顶级View也叫根View,顶级View一般都是ViewGroup。
这样就与上面ViewGroup的事件分发连接上了。
借用何俊林大神(逆流的鱼yuiop)的一张图片,来展示ViewGroup的事件分发流程。
ViewGroup事件分发试验
code
与博客最开始一样,自定义一个MyLayout,继承LinearLayout。重写三个方法:onInterceptTouchEvent、onTouchEvent、dispatchTouchEvent,代码如下:
然后,自定义一个MButton,继承Button,重写两个方法onTouchEvent、dispatchTouchEvent,代码如下:
在布局文件activity_view_group.xml中引用MyLayout和MButton,如下所示:
最后,在ActivityViewGroup中为MyLayout和MButton设置onTouchListener,重写Activity的onTouchEvent、dispatchTouchEvent。代码如下:
代码完成后,运行!接下来进行试验。
试验
手指点击一下手机屏幕中的按钮
log打印如下:
运用之前的分析结果,可解释为:
手指按下
ActivityViewGroup
· 接收到DOWN事件
· 调用dispatchTouchEvent分发事件(①Activity: dispatchTouchEvent)
- - - - - - - - - - - - - - - -
Activity会将事件传递给顶层View——DecorView ,DecorView 继承了FrameLayout。DecorView默认不拦截事件,会继续向下分发事件,最后事件进入我们的根布局MyLayout
- - - - - - - - - - - - - - - -
MyLayout
· 接收到DOWN事件
· 调用dispatchTouchEvent进行事件分发(②MyLayout: dispatchTouchEvent)
· 调用onInterceptTouchEvent,默认不拦截事件(③MyLayout: onInterceptTouchEvent)
· 向下遍历查找触摸点所在位置可接收事件的子View,找到了MButton,将事件传递给MButton
- - - - - - - - - - - - - - - -
MyButton
· 接收到DOWN事件
· 调用dispatchTouchEvent进行事件分发(④MButton: dispatchTouchEvent)
· 先进入触摸监听的重写方法onTouch(⑤MButton: onTouch),返回false
· 后进入onTouchEvent最终处理事件(⑥MButton: onTouchEvent)
手指抬起(UP)
与DOWN事件一样,从外向内传递事件,最终由被点击的MButton的onTouchEvent将事件处理。
手指点击一下空白处
按照我们之前的分析,点击空白处,MyLayout没有子View去接收事件,事件就由MyLayout(作为View)处理事件。到底是不是呢?看一下log打印验证一下:
从上面log打印第4行看,这里走了MyLayout设置的onTouchListener监听的方法onTouch,这个方法返回默认值false,接着走了MyLayout的onTouchEvent, 根据上一篇笔记:事件分发机制(一):View的事件分发,我们知道,当一个控件不可点击时,在View的onTouchEvent中不会处理action,会返回false。onTouchEvent返回false,那么MyLayout(此时作为View)的dispatchTouchEvent也会返回false,由View的dispatchTouchEvent中的代码
可知,onTouchEvent(event)返回false,result就不会赋true值(见上面代码最后三行),那么dispatchTouchEvent就返回false,那么MyLayout的dispatchTouchEvent就返回false,也就是说MyLayout也处理不了这个点击事件。
那么最后就会传递给(其实先交给了DecorView,逻辑与MyLayout应该相同,最后传递给Activity处理)Activity的onTouchEvent处理,最终Activity作为处理事件的对象,之后的一序列动作(down之后的move和up)都将由Activity的onTouchEvent处理。
如果我们给MyLayout的布局中①设置android:clickable="true",或②在onTouch中返回true,事件就会交由MyLayout处理。运行后拿到log打印验证一下:
- ①设置android:clickable="true" (或 在onTouchEvent中返回true)
- ②在onTouch中返回true
ViewGroup拦截事件
首先将上面的设置恢复。
从上面的log打印发现,事件由Activity的dispatchTouchEvent接收,并分发到MyLayout的dispatchTouchEvent,在MyLayout开始分发后先调用了onInterceptTouchEvent,然后再继续事件分发。那么,我们就再次尝试一下事件拦截,看一看log打印结果。
将MyLayout的onInterceptTouchEvent返回值改为true,如下
运行后,分别点击按钮和空白区域,发现log打印一样
可以解释为:
·> 事件传递到Activity的dispatchTouchEvent;
·> 分发给MyLayout的dispatchTouchEvent;
·> 先经过MyLayout拦截onInterceptTouchEvent,返回true,事件被拦截;
·> MyLayout作为View来处理事件,事件先经过onTouch,返回false,
·> 后传递到MyLayout的onTouchEvent;
·> 由于MyLayout默认不可点击,MyLayout的onTouchEvent无法处理事件,会返回false,事件没有被MyLayout处理,dispatchTouchEvent返回false;
·> 最终,事件由Activity在它的onTouchEvent中处理,后续的move、up等一序列事件都在Activity的onTouchEvent中处理。
MyLayout有事件拦截的时候,点击空白区域的流程和点击按钮的流程是一样的。
我们想让MyLayout处理事件,那就需要让MyLayout是clickable,或让onTouchEvent处理过事件返回true,或由MyLayout的onTouch处理过事件后返回true。如果直接由dispatchTouchEvent处理事件后返回true,那么事件拦截接方法onInterceptTouchEvent也不会走了。
我们来看一下log打印验证 一下!
-①设置android:clickable="true" (或 在onTouchEvent中返回true)
- ②在onTouch中返回true
- ③直接在dispatchTouchEvent处理事件后返回true
注意: 直接在MyLayout(ViewGroup)的onTouch中返回true,或在onTouchEvent中返回true,或让它clickable,MyLayout是不能获得事件并执行的;除非事件被MyLayout拦截(onInterceptTouchEvent返回true),或在点击处没找到能够处理事件的子View(包括点击空白处),MyLayout才作为View去处理事件,先经过onTouch(如果返回true,表示事件被onTouch处理,不再经过onTouchEvent),再经过onTouchEvent。
