引子
烟雨红尘 https://wap.zuxs.net/Android事件分发其实是老生常谈了,但是说实话,我觉得很多人都只是懂其大概,模棱两可。不信我可以先抛出几个问题:
- ACTION_DOWN和其他触摸事件的处理方式一样吗?如果不,有什么不同之处?
- 为什么如果所有子View都不消费ACTION_DOWN事件,后续的MOVE和UP事件就被拦截了?
- 子View能干预父View对ACTION_DOWN事件的拦截吗?
- 如果有多个View都包含触摸坐标,它们都能接收到事件分发吗?如果不是,谁会接受?原理?
- 多指操作怎么处理的?
相比很少能完全答上来吧。多数文章都在讲事件分发的递归调用链,确实那个很重要,但不够深入,比如都没有把ACTION_DOWN事件单独拿出来讲。如果真想彻底弄懂分发原理,碰到分发难题时想办法解决,必须得把源码的思路理清楚,然后才能对证下药。本文的目的就是从源码层次梳理一下,重点放在ViewGroup的dispatchTouchEvent方法上,这个方法是事件分发的核心中的核心!我们借此以小见大,理解事件分发的机制。ps,本文着重在源码和分析,就不怎么画图了(其实是懒),大家可以看网上相关图片,随便一搜很多。本文力求深入浅出,我来深入源码,然后尽量用浅显的语言讲出来。
先简单讲一下事件分发的源头
很多人讲事件分发,都说其开始是从Activity的dispatchTouchEvent开始的,大家可以简单这么理解,但是肯定会有人疑问,Activity的这个方法从哪儿调用的呢?我写了一个简单的Demo,然后在Activity的dispatchTouchEvent方法里加了一个断点得到其函数调用栈,看下图:
好家伙,原来Activity分发之前还有这么多过程,简单梳理了一下:大概是从InputEventReceiver开始,经过ViewRootImpl,里面各种InputStage调用之后,最后给了DecorView,然后DecorView传给的Activity。其实这里挺有意思的,本来DecorView先获取到事件的,但是后来它又分配给了Activity,Activity之后又通过phoneWindow把事件传回给了DecorView,一来一回,就是为了让Activity去处理一下事件而已。Activity传给DecorView之后,DecorView会调用superDispatchTouchEvent
方法:
public boolean superDispatchTouchEvent(MotionEvent event){
return super.dispatchTouchEvent(event);
}
因为DecorView是一个FrameLayout,它最终还是调用了我们熟悉的ViewGroup的dispatchTouchEvent(),这也是本文的主角。所谓的事件分发,本质上就是一个递归函数的调用,这个递归函数就是dispatchTouchEvent,至于onIntercepterTouchEvent,onTouchEvent,OnTouchListener,onClickListener...balabala都是在这个递归函数里面的操作而已,最核心,最骨干的还是dispatchTouchEvent,所以我们来分析它:
ViewGroup的事件分发
大家应该或多或少读过其源码,源码虽然不是太长,但乍一看还是会头大的,我想大多数人可能大概看懂了其逻辑,对于里面很多东西不明所以。比如mFirstTouchTarget是干嘛的?临时变量alreadyDispatchedToNewTouchTarget是干嘛的?里面好像有链表啊,干嘛使的?
这里稍微补充一句,对于事件分发来说,从用户按下到抬起,这是一组事件,以ACTION_DOWN为开头,UP或CANCEL结束。我们后面分析的也是这一组事件。
源码较长,我写了伪代码给大家看看,说是伪代码,其实还是比较全面详细的,省略了部分函数参数,但重点的代码都包含了,重点看注释。如果嫌长,可以直接先看后面的结论,再回头看伪代码。
//本源码来自 api 28,不同版本略有不同。
public boolean dispatchTouchEvent(MotionEvent ev) {
// 第一步:处理拦截
boolean intercepted;
// 注意这个条件,后者代表着有子view消费事件。后面会讲
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 子view调用了parent.requestDisallowInterceptTouchEvent干预父布局的拦截,不让它爸拦截它
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
//既不是DOWN事件,mFirstTouchTarget还是null,这种情况挺常见:如果ViewGroup的所有的子View都不消费 //事件,那么当ACTION_MOVE等非DOWN事件到来时,都被拦截了。
intercepted = true;
}
// 第二步,分发ACTION_DOWN
boolean handled = false;
boolean alreadyDispatchedToNewTouchTarget = false; //注意这个变量,会用到
// 不拦截才会分发它,如果拦截了,就不分发ACTION_DOWN了
if (!intercepted) {
//处理DOWN事件,捕获第一个被触摸的mFirstTouchTarget,mFirstTouchTarget很重要,
保存了消费了ACTION_DOWN事件的子view
if (ev.getAction == MotionEvent.ACTION_DOWN) {
//遍历所有子view(看源码知子View是按照Z轴排好序的)
for (int i = childrenCount - 1; i >= 0; i--) {
//子view如果:1.不包含事件坐标 2. 在动画 则跳过
if (!isTransformedTouchPointInView() || !canViewReceivePointerEvents()) {
continue;
}
//将事件传递给子view的坐标空间,并且判断该子view是否消费这个触摸事件(分发Down事件)
if (dispatchTransformedTouchEvent()) {
//将该view加入头节点,并且赋值给mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
//第三步:分发非DOWN事件
//如果没有子view捕获ACTION_DOWN,则交给本ViewGroup处理这个事件。我们看到,这里并没有判断是否拦截,
//为什么呢?因为如果拦截的话,上面的代码不会执行,就会导致mFirstTouchTarget== null,于是就走下面第一 //个条件里的逻辑了
if (mFirstTouchTarget == null) {
super.dispatchTouchEvent(ev); //调用View的dispatchTouchEvent,也就是自己处理
} else {
//遍历touchTargets链表,依次分发事件
TouchTarget target = mFirstTouchTarget;
while (target != null) {
if (alreadyDispatchedToNewTouchTarget) {
handled = true
} else {
if (dispatchTransformedTouchEvent()) {
handled = true;
}
target = target.next;
}
}
}
//处理ACTION_UP和CANCEL,手指抬起来以后相关变量重置
if (ev.getAction == MotionEvent.ACTION_UP) {
reset();
}
}
return handled;
}
总结一下:ViewGroup事件分发分为三步
第一步:判断要不要拦截:这里的条件分支要看清,外层的判断语句意思是,要么肯定会拦截,要么可能不拦截,可能不拦截的话需要满足以下两个条件之一:
事件是DOWN事件。
非DOWN事件也可以,但是需要满足mFirstTouchTarget != null 。这个条件意味着什么呢?意味着在之前的DOWN事件中,至少有一个子View捕获(消费)了DOWN事件,也就是意味着对于这一组分发事件来说,有子View愿意处理这个事件。
在可能拦截的情况下,我们进入拦截判断流程,很简单: 先看子view有没有调parent.requestDisallowIntercept,如果调用了,不拦截,没有的话走到onIntercepteTouchEvent方法,根据其返回值决定是否拦截。
第二步:如果没有拦截,分发DOWN事件:遍历所有子View,查看触摸区域是否有子view有资格消费这个事件,判断依据有二:子View不能在动画?触摸点坐标得落在子View的范围内。如果前两者都满足,则将DOWN事件分发给子View,这一步引出了一个重要的方法:
dispatchTransformedTouchEvent
,这个方法干的活就是最重要的事情:分发给子view,也就是说,这个方法进行了递归的调用,感兴趣的同学可以自己阅读其源码。遍历的范围是什么呢?源码中告诉我们是按照z轴顺序排列好的一个view list,这里的z轴顺序保证了我们先把事件分发给z轴坐标大的值,也就是更靠外层的view。另外,这个分发方法有个返回值,如果为true,则为mFirstTouchTarget赋值,否则其值仍为null。最后有个方法,addTouchTarget,这个方法一方面为mFirstTouchTarget赋值,另外也构建了一个链表,链表保存的什么呢?其实这个跟多指操作有关,它保存了所有“mFirstTouchTarget”,mFirstTouchTarget是啥呢?其实字面意思很明白了,就是手指碰触的位置最外层的View,对于多个手指来说,每个手指都会有一个mFirstTouchTarget,于是就保存到了这个链表中了。为什么要保存mFirstTouchTarget呢?很简单,为了让后续的该事件组的其他事件知道谁要处理事件啊!要不然总不能每个事件都要判断一下,那效率就低多了。这里就得出来一个结论,Down事件的分发决定了那个view要捕获事件,如果捕获了,后续的事件就直接分发给它,也就是说move up等事件的分发交给谁,取决于它们的起始事件Down由谁捕获。第三步:分发其他事件:首先判断mFirstTouchTarget,如果为null,说明前一步的DOWN事件没有子view消费掉,这种情况表示该ViewGroup的孩子View都不打算处理事件,这种情况自然要交给ViewGroup自身处理,代码里交给了super.dispatchTouchEvent,也就是调用了ViewGroup的父类View处理(onTouchEvent)。如果不为null,说明有子View要处理事件,进入else语句里,把事件分发下去。 这里眼尖的读者应该看到了,第二步不会已经分发了DOWN事件了吗,这里为啥还要再分发一次呢?不重复了吗,这里就到了前面讲的另外一个变量出场了,
alreadyDispatchedToNewTouchTarget
,这个变量在伪代码里第二步的开头提到了,当第二步里有子View消费了事件后,该变量会变成true,此时第三步会判断该值,如果为true,就直接返回handle=true,不再分发事件了。这就避免了DOWN事件被两次分发。对于其他事件,这个变量肯定是false,所以一定会走else的逻辑,进行分发。
再简化一下,加点大白话:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean intercepted = false;
if (DOWN 或者 DOWN的时候没有孩子想处理) {
if (孩子不让拦截?) {
intercepted = false;
} else {
intercepted = onIntercept();
}
} else {
intercepted = true;
}
if (DOWN && !intercepted) {
for (遍历孩子View) {
if(如果该孩子能消费就给分发给它,如果它真消费了DOWN事件){
给mFirstTouchTarget赋值 ;
Down事件已经分发了;
跳出循环;
}
}
}
if (mFirstTouchTarget == null) {
孩子都不想消费,交给我自己处理吧;
} else {
while(遍历所有孩子,将事件分发下去) {
if (DOWN事件已经分发了) {
return true;
}else {
分发给之前保存的mFirstTouchTarget对应的子View;
}
}
}
}
到这里,我们就把ViewGroup的事件分发讲完了,接下来分析一下View的dispatchTouchEvent
View的事件分发
View的非常简单
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onTouchListener.onTouch()) {
result = true;
}
if (!result && onTouchEvent()) {
result = true;
}
return result;
}
可见,先判断listener,如果listener返回true了,onTouchEvent就不进入了,否则,走onTouchEvent方法。
View的复杂点的地方在onTouchEvent方法的默认实现里,里面处理了很多onClick,onLongclick事件的逻辑,感兴趣的同学可以自行阅读源码,这里只说一点,一旦设置了onClickListener或者onLongclickListener,那么onTouchEvent就会返回true,也就是消费,其他情况下默认不消费,源码里这么写的
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
clickable为true,则返回true,否则返回false。
举个例子练习一下
问题很简单,一个FrameLayout中间放了一个按钮,Framelayout和按钮都添加了点击事件,那么,请问点击按钮和点击按钮之外的区域事件分发过程是怎样的?
先看按钮之外: FrameLayout是一个ViewGroup,而且没有重写dispatchTouchEvent方法。根据以上分析:
- 第一步,down来了以后,进入拦截逻辑,framelayout不拦截,所以intercepted == false
- 第二步,处理down事件,发现触摸点没有子view,所以不会有人处理这个事件的,mFirstTouchTarget == null
- 第三步,交给自身处理,自身会调用onTouchEvent,在这里由于设置了clickListener,返回true,消费了事件。
- 后续move和up,由于mFirstTouchTarget == null,第一步会拦截,所以直接交给自身处理,同上面的第三步,同时,up的时候会响应click事件。
按钮内:
- 第一步,同上
- 第二步, 发现触摸点有子view,mFirstTouchTarget != null,且将DOWN事件分发给了子View。
- 第三步,mFirstTouchTarget非null,但
alreadyDispatchedToNewTouchTarget
这个变量为true,所以直接返回true。 - 后续move和up,第一步不会拦截,因为不是down事件所以第二步跳过,第三步将事件分发给了子View,子View响应了点击事件,返回true,而这个过程中,ViewGroup没有消费任何事件,所以自然不会响应onClick事件。
这样,是不是就解释了两层View都添加click事件时的响应结果了~
总结
总的来说,事件分发分两步,拦截和分发,其中分发有两种情况,Down事件和非Down事件,down事件是事件链的起点,决定了要不要消费事件,而且将消费的子View保存下来给后面使用。如果所有的子View都不消费down事件或者压根没有子View,会使得mFirstTouchTarget为null,后面的所有事件就不再分发给子view了,直接由本view group处理。当然这里的交给本人处理,实际上可能它也不消费,会继续往上传,最终“归”到Activity处理。
越来越感到读源码的重要性,Let's read the fucking sourceCode!