下篇:探究ViewGroup的事件分发
ViewGroup是一组View的集合,它包含多个子View和子VewGroup,
是所有布局的父类或间接父类,比如LinearLayout、RelativeLayout等都是继承自ViewGroup。
ViewGroup实际上也是一个View,只不过比View多了可以包含子View和定义布局参数的功能。
首先我们自定义一个布局,命名为MyLayout,继承自LinearLayout,如下所示:
然后在主布局文件activity_main.xml中加入自定义布局MyLayout,并在MyLayout中添加两个按钮,
接着在MainActivity中为MyLayout和这两个按钮注册监听事件:
运行程序,分别点击Button1、Button2和空白区域,打印结果如下所示:
我们发现,当点击按钮时,MyLayout注册的onTouch方法并不会被执行,只有点击空白区域的时候该方法才会被执行。
我们可以先理解成点击事件被按钮的onClick方法消耗掉了,因而不会再继续向下传递。
准确来说:onClick方法只有在onTouchEvent的ACTION_UP分支里才会被触发。
那是否说明事件传递的顺序是先经过View,再传递到ViewGroup?(爆料:显然不是)
ViewGroup中有一个onInterceptTouchEvent方法,我们来看一下这个方法:
只有一行!返回了一个false。很显然:ViewGroup默认不拦截任何事件。
那么我们在MyLayout中重写这个方法,然后返回一个true会怎样呢?
再次运行程序,分别点击Button1、Button2和空白区域,打印结果如下所示:
我们发现,不管点击哪里,永远都只会触发MyLayout的touch事件,按钮的click事件完全被拦截掉了!
这完全推翻了我们之前的猜想:如果事件传递的顺序是先经过View,再传递到ViewGroup,
那么按钮的click事件是不可能被MyLayout拦截掉的。
事实上事件传递的顺序是先经过ViewGroup,再传递到View。
上篇我们提到,只要点击了某个控件,该控件的dispatchTouchEvent方法就会被调用。
我们在这里加以补充:当你点击了某个控件,首先会调用该控件所在布局的dispatchTouchEvent方法,
然后在该方法中找到被点击的控件,继而调用该控件的dispatchTouchEvent方法。
dispatchTouchEvent方法只负责事件的分发,返回true时事件的传递将被中断。
然而自定义控件MyLayout中并没有dispatchTouchEvent这个方法,
于是最终找到其父类ViewGroup的dispatchTouchEvent方法:
这个方法貌似有些复杂,我们依然只挑重点看。
首先在第13行的if判断中有两个值:第一个值disallowIntercept是指是否禁用事件拦截功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对该值修改。
那么当第一个值为false的时候,第二个值就决定了是否可以进入到if判断的内部。
第二个值是什么呢?竟然就是对onInterceptTouchEvent方法的返回值取反!
也就是说这个返回值取false还是true,决定了是进入到这个判断的内部还是跳出这个判断。
由于刚刚在MyLayout中重写了onInterceptTouchEvent方法,让该方法返回true,导致跳出了if判断,然后所有按钮的click事件都被拦截掉了(根据打印结果2),ViewGroup将会自己处理(如何处理呢)。
那么我们完全有理由相信,按钮click事件的处理就是在这个第13行 if判断的内部进行的!
那么我们重点来看一下这个if判断的内部实现(用于解释打印结果1)。
首先在第19行通过一个for循环,遍历了当前ViewGroup下所有的子View。
然后在第24行判断当前遍历的View是否为正在被点击的View,如果是就进入到该条件判断的内部。
接着在第29行调用了该View的dispatchTouchEvent方法(处理按钮的click事件)。
我们知道View的dispatchTouchEvent方法是有返回值的。
并且如果控件是可点击的,那么点击该控件时,dispatchTouchEvent方法的返回值必为true。
因此会导致第29行的条件判断成立,于是在第31行给ViewGroup的dispatchTouchEvent方法直接返回了true,
这样就导致后面的代码无法被执行。
这样恰恰验证了第一次的打印结果:如果按钮的click事件得到执行,就会把MyLayout的touch事件拦截掉。
这说明:点击事件一旦交给子元素处理(被消耗),父元素将无法接收到任何事件。
如果点击的是空白区域呢?(根据打印结果1、2)
很显然,这种情况会跳过第31行,继续执行后面的代码。
于是来到第44行,如果target等于null,就会进入到该条件判断的内部。
一般情况下该条件都会成立,因此会在第50行调用super.dispatchTouchEvent方法。
该方法就是View中的dispatchTouchEvent方法(因为ViewGroup的父类就是View)。
之后的处理逻辑参照上篇可知:MyLayout注册touch事件时的onTouch方法将会得到执行。
(续)如果点击事件被ViewGroup拦截掉了,自己是如何处理的呢?
因为直接跳出了第13行的if判断,跟上面的情况一样,将会继续执行后面的代码…
很显然,ViewGroup的dispatchTouchEvent方法的返回值将取决于对该事件的处理。