三个维度,不太一样的View事件分发机制(理论概念)
每一个Android开发者,都绕不开View的事件分发,理解好View的事件分发机制,有利于我们解决各种与设备触摸交互的问题,同时也利于我们实现更复杂、炫酷的自定义View效果。
关于Android View的事件分发机制,笔者打算分为三篇文章来描述,分别为:
(1)Android View事件分发机制之概念理论篇
(2)Android View事件分发机制之源码解读篇
(3)Android View事件分发机制之滑动冲突实战解决篇
本文是第一篇:Android View事件分发机制之概念理论篇
为何称为事件分发
且看一段伪代码:
@Override
public boolean onTouch(View view, MotionEvent event) {
// 处理我们的手势触摸交互
return true;
}
这个方法相信大家都不陌生,我们在处理触摸屏幕手势交互时,就会重写onTouch方法,onTouch方法中,第二个参数叫MotionEvent,其义为手势行为事件。而通过onTouch的回调调用栈及源码可发现,onTouch被回调前,经过了多重“关卡”的判断、验证、处理,MotionEvent对象也被层层传递与处理,最终才执行到了当前View的onTouch方法。即:
我们与设备产生的交互,即触摸(点击、长按、拖动等)手势,再到应用界面的响应,整个过程都是一个MotionEvent对象的传递以及对其处理的过程,MotionEvent即手势行为事件,故称为:事件分发。
事件分发机制理论
首先,我们先了解几个概念:
注:尽管ViewGroup也是一种View,但在下面的描述中,我们把ViewGroup和View区分开,ViewGroup可以包含其他的View和ViewGroup,而View则不可以包含,View是某一个特定的View控件。
MotionEvent的四种基本状态
-
ACTION_DOWN : 手指接触屏幕的瞬间
-
ACTION_MOVE :手指在屏幕上移动
-
ACTION_UP : 手指离开屏幕的瞬间
-
ACTION_CANCEL : 取消手势
其中,ACTION_CANCEL事件需要在某种特定情况下才会产生,比如出现滑动冲突时,子View就会收到一个ACTION_CANCEL事件,但本文先不做讲述,会在滑动冲突分析解决篇做一下介绍。
事件序列的生命周期
手指从按下接触屏幕到,抬起离开屏幕,这个过程包含了一个ACTION_DOWN事件,一个ACTION_UP事件,以及多个ACTION_MOVE事件,这一串事件,我们成为一个事件序列,这个事件的生命周期由ACTION_DOWN开始,并由ACTION_UP(或者ACTION_CANCEL)结束。
View的事件分发机制,可分为三部分来理解:事件分发、事件拦截、事件响应,分别对应着三个方法:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent,下面详细描述。
事件分发
dispatchTouchEvent()
true:消耗(处理)当前事件,并终止传递
false:不消耗(不处理)当前事件,事件向子元素下传
手势交互发生在某一个指定View上,产生的事件序列并进行分发,必然会有一个起点和一个终点,Activity为起点,我们触摸的那个View为终点,起点到终点的过程,还经过了多层传递,传递顺序如下:
Activity -> Window -> ViewGroup -> ViewGroup -> … (n个ViewGroup ) -> View
事件拦截
onInterceptTouchEvent()
true:拦截当前事件,并终止传递
false:不拦截当前事件,事件向子元素下传
既然我们的手势是与这个指定的View进行交互,那么我们的预期应该是整个事件序列的所有事件,都应该被传递到指定的View,并由View做出响应。但是在这个事件序列进行分发传递的过程中,ViewGroup ,即指定View的父亲可以对某个或者某几个事件拦截下来,并终止传递给下一级。因此,事件在进行分发过程中,就有可能会出现事件被拦截的情况,也因此可能会出现不符合我们预期的手势交互响应情况。而有些时候,ViewGroup拦截事件,也可能是人为故意拦截,用以实现某些功能逻辑和效果。
从事件分发传递的顺序就不难得出,View不存在onInterceptTouchEvent()方法,即View不会去拦截任何事件,直接消耗处理事件,或者不处理(通知上层父元素处理)。因为View根本不存在子元素了,拦截没有任何意义。而这点也可以通过SDK源码得以证明。
事件响应
onTouchEvent()
true:消耗(处理)当前事件,并反馈给上层父元素,告知已消耗处理
false:不消耗(处理)当前事件,同时反馈给上层父元素,让父元素消耗处理
真正消耗处理事件,则在onTouchEvent方法中处理,在这个方法中,会判断传进来的MotionEvent状态,并对指定的状态进行处理,如点击、长按、双击、触摸移动。一般地,我们在处理MotionEvent对象后,都会返回true,表示此次的MotionEvent已经被消耗并处理,这样的话,这个事件所属的事件序列的后续事件才会继续传递到当前处理的View。如果这里不想消耗处理事件,则回调父元素的相关方法,让父元素来消耗处理,如果一直都不消耗,最终会传回到Activity,如果Activity也不消耗,那么这个事件就消失了。
读到这里,你可能会有一个疑问:dispatchTouchEvent返回结果的含义,似乎与onInterceptTouchEvent、onTouchEvent存在冲突的地方,其实不然,它们三者之间的关系可用下面的伪代码来表示:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean comsune = false;
if (onInterceptTouchEvent(event)) {
comsune = onTouchEvent(event);
} else {
comsune = child.dispatchTouchEvent(event);
}
return comsune;
}
这段话伪代码很好理解,也把三者之间的关系表现的很直白。我们简单描述一下:
dispatchTouchEvent的返回结果,表示当前View是否消耗事件,而它是否消耗,则由自身的onTouchEvent和子元素的dispatchTouchEvent来决定。而自身的onTouchEvent是否被得到执行,则取决于自身是否对事件进行拦截,即onInterceptTouchEvent是否返回true,如果拦截,才由自身的onTouchEvent处理,否则由子元素的dispatchTouchEvent处理;当事件传到了子元素的dispatchTouchEvent,处理过程又是一样的,直到事件被传到终点,或者在某一层被拦截,才终止传递分发,然后做出响应。
这里需要弄明白的整个事件分发和响应的设计思路:
从上述伪代码和描述中得知,如果当前ViewGroup不消耗,但是它的子元素消耗了,那么对于当前ViewGroup的父元素来说,就是由当前ViewGroup消耗了。如果有点绕,不妨假设有个事件传递链:
A -> B -> C
被箭头指向的,作为子元素。一个事件从A分发到B,但是B不处理,B继续分发给C,C接收到后进行消耗处理了,并把消耗结果告诉B,B接收到这个结果后也要告诉A。但由于A是直接跟B打交道的,A只知道在B那一层事件被消耗了,并不知道其他的具体细节,所以,对于A来说,就是由B进行消耗处理了。
我们再举一个更直白的例子:
假设你上头有一个经理,自己又有一个手下,经理交给你一个任务,如果你自己能完成并且想去完成,则自己直接处理,并报告上级经理;或者你自己不处理,交给你的手下处理,如果你的手下完成了你交给他的任务,也相当于你完成你经理交给你的任务;如果你手下没有能力去完成,那么就会告诉你他没法处理,然后让你处理,如果你自己也不处理或者没法处理,你就只能继续反馈给你的经理说没法处理了,后续的情况,就是你经理去负责了。
因此,我们可以得知:
- 事件分发 - 由外到内
- 事件响应 - 由内到外
事件分发机制流程图
文章读到这里,View的事件分发机制理论就了解得差不多了,我们可以用一个流程图来描述:
本文到这里就差不多结束啦,相信你对View的事件分发机制已有了大致的了解和掌握,下一篇文章中,将会从SDK源码角度去分析和解读View的事件分发机制,以更深层次的了解其工作原理!
这里不妨先引出几个问题:
(1)如果一个View同时监听onClickListener和onTouchListener ,哪个会先被执行呢?
(2)onTouch()和onTouchEvent()有什么区别?如何去区分呢?
(3)子View能否阻止父对事件的拦截?如果能,该如何处理?
我们下一篇文章解读~
THE END
本文地址:https://blog.csdn.net/h_xiao_x/article/details/108188606