android事件分发
事件分发在Android中非常重要,在滑动冲突,下拉刷新,嵌套滑动的时候都需要非常清楚事件分发的机制,才能写好对应的处理代码。曾经以为我对事件分发已经很清楚了,也写过几篇文章,但是总感觉没有完全说清楚,今天再从代码的角度分析一遍事件分发机制,希望以后遇到所有事件分发的问题,都能在这里找到答案。
先看几个问题,如果这些问题你都知道答案,那本篇文章就不用看了。
问题
1、如果拦截了某个事件,是否就会交由本view的View:dispatchTouchEvent处理?
2、一个事件,如果子view处理失败,是否就交还给父view处理?
3、如果一个down事件,大家都不处理,会怎么样?
4、parent把事件传递给哪个子view呢?是根据位置查一遍的吗?
5、某个view成功处理down事件,但是处理move事件失败,会如何?
神圣的规则
规则1:事件传递由父控件传递到子控件,事件消费是子控件优先。
规则2:down事件,子控件如果不消费,就还给父控件。
规则3:我是一个坏父亲,父亲吃到肉了,绝不会再给儿子,儿子吃到肉了,父亲还可能抢。
手指从按下到抬起,我们称为一个cycle,以DOWN事件开始,UP事件结束,里面有若干个MOVE事件。一个cycle内,v1处理了某事件,后边的事件绝不会被v1的child处理,v1肯定会拦下来。
很多文章在介绍事件分发的时候,都会提到onTouch或者onTouchEvent,本文不会说这2个,因为这2个都是View的dispatchTouchEvent方法内,本文只会提到dispatchTouchEvent方法,这样更准确一点。当然,其实大部分情况下,View的dispatchTouchEvent就是调用onTouchEvent,一般onTouch是没有的,这块的逻辑如果不清楚的话,可以看android点击事件(View)。
down事件分发
在讲述事件分发的流程前,先定义三个角色,p,pp,c其中p为主角ViewGroup,pp是p的parent,c为p的child。
手指按下就会触发down事件。例如我们点击了一个TextView,down事件会从activity开始传递,然后传递给DecorView,接着往下传递给对应的ViewGroup,一层层传下来直到TextView。
触摸了任何一个ViewGroup都会调用ViewGroup的dispatchTouchEvent。首先会先进入onInterceptTouchEvent,如果返回true的话,就拦截了,交由本viewgroup的View::dispatchTouchEvent方法,注意这里和前面的dispatchTouchEvent方法不一样,一个是View的dispatchTouchEvent,一个是Viewgroup的dispatchTouchEvent。View的dispatchTouchEvent我们在android点击事件(View)详细说过了,而Viewgroup的dispatchTouchEvent就是负责事件分发的核心代码,也就是我们这篇文章的主要内容,看明白了这个函数的200多行代码,事件分发的所有问题都能明白。
结合下边的图,我们可以明白down事件的传递机制。
MacDown logo
1、如果p的onInterceptTouchEvent返回true,那就直接拦截,交给p的View::dispatchTouchEvent处理,流程图中的super.dispatchTouchEvent就是指View::dispatchTouchEvent,后面的流程暂时不说;
2、如果p的onInterceptTouchEvent返回false,那就不拦截,继续查点击到了哪个child,如果查不到,那就还是交给p的View::dispatchTouchEvent处理。如果查到了,那就交给这个child(简称c)的dispatchTouchEvent处理。c的dispatchTouchEvent有2种结果,true和false,如果返回false,那还是交给p的View::dispatchTouchEvent处理;如果返回了true表示c已经处理好了这个事件,那p就很开心了,小弟帮我完成了一件事,记下他的功劳,把mFirstTouchTarget进行赋值,指向c,然后p的dispatchTouchEvent返回true。
刚才交给p的View::dispatchTouchEvent处理,后面的流程还没说。从前面的流程可以看到,走到这里有3种原因。1、onInterceptTouchEvent返回了true,拦截了;2、点击的这个点没有对应的child;3、c没有处理好,返回了false。p的View::dispatchTouchEvent也只有2种结果,true或者false,如果返回了true,那整个p的dispatchTouchEvent就返回了true,但是此时mFirstTouchTarget为null,因为是自己处理的,不是child处理的。如果p的View::dispatchTouchEvent返回了false,那整个p的dispatchTouchEvent就返回false。
此时p的dispatchTouchEvent结束了,结束的时候会返回true或者false,那后面会发生什么呢?我们看这个流程图,要有递归的思想。此时p: dispatchTouchEvent完成,其实和c:dispatchTouchEvent()是一样的,要把结果告诉pp(p的parent)。
此时的状态有3种:
状态1:p: dispatchTouchEvent()返回true,并且p的mFirstTouchTarget空,代表是p处理了事件down
状态 2:p: dispatchTouchEvent()返回false
状态3:p: dispatchTouchEvent()返回true,并且p的mFirstTouchTarget非空,代表p的child处理了事件。
用mFirstTouchTarget记录有什么好处呢?想想,如果很上层的view想知道到底谁立下了如此大功,处理了事件,顺着mFirstTouchTarget找过来就行了,其实只有在down事件的时候会根据按下的位置来查找对应的子view,后面的事件都是根据mFirstTouchTarget来查找的,这样明显提高效率。
MOVE的事件分发
我们先回头看下,down事件结束之后的三种状态,其实可以合并成2种状态。
先看状态1,p: dispatchTouchEvent()返回true,那么pp的dispatchTouchEvent()肯定也返回true,并且pp的mFirstTouchTarget指向p,看看这个是不是和状态3类似的,只是p换成了pp。 这种情况我们称为case1,case1的本质是什么?有人成功处理了down事件。
再看状态2,p: dispatchTouchEvent()返回false,会来到pp的dispatchTouchEvent()代码内,pp的dispatchTouchEvent()可能返回true或者false,如果返回了true,那其实和case1类似了。如果还是返回false,那就继续往上传,只要祖宗有一个返回了true,那就掉入了case1.如果大家坚持返回false,那就会一直传到DecorView。这种情况我们称为case2,本质就是无人成功处理down事件。
无人成功处理down事件
无人处理down事件,比较简单,我们先说,发生的概率也很小。没有人处理down事件,这个事件就会一直往上抛,直到PhoneWindow$DecorView。而DecorView的onTouchEvent一般返回false,DecorView的mFirstTouchTarget为null。下一次move事件来了,直接拦截并且自己处理。所以结果就是后面的所有事件都停在了DecorView,不会下传,而DecorView的处理结果就是false。所以这种情况下,后面的事件都不会被处理,可以认为被丢弃了。
有人成功处理down事件
假设有view族谱p1,p2,...pn,后面一个是前面一个的parent。假设p2处理了down事件,那么我们根据规则3,move事件不可能给p1,所以我们不用考虑p1。此时p2的mFirstTouchTarget为null,p3,p4等的mFirstTouchTarget非空。所以此时有2种类型的view要考虑,第一种是p2类型的,mFirstTouchTarget为null;第二种是p3,p4类型的,mFirstTouchTarget非空。
move事件的传递,可以分为2个阶段,第一阶段就是决定intecepted的值,第二阶段就是根据intecepted的值进行事件分发.
MOVE事件第一阶段
第一阶段流程图如下所示。
第一种情况,mFirstTouchTarget为null,intecepted直接会变为true,拦截所有事件,这就是规则3的来源。
第二种情况,mFirstTouchTarget非空,会根据disallowIntercept标志和onInterceptTouchEvent()来决定intecepted的值。
move事件第二阶段
第二阶段流程图如下所示
此时可以分3个case来看
case1
先看mFirstTouchTarget为空的情况,那么他的intecepted必定是true,会调用View:dispatchTouchEvent()作为返回值
case2
若mFirstTouchTarget非空,intecepted为false,此时按理说会去找对应位置的child,NONONO。这里的逻辑和down事件不一样,这里不会根据位置去找,而是根据mFirstTouchTarget去找,因为我们down事件的child已经记录在mFirstTouchTarget内了,所以直接找mFirstTouchTarget就行。(其实mFirstTouchTarget其实是个链表,跟着链表爬一遍)。mFirstTouchTarget的处理结果就作为整个dispatchTouchEvent的返回结果。
case3
若mFirstTouchTarget非空,intecepted为true,他会给mFirstTouchTarget指向的view发一个cancel事件,然后mFirstTouchTarget置null,然后返回true。啊??居然不调用自己的View:dispatchTouchEvent吗?确实是的。本次move事件,实际上不会调用自己的View:dispatchTouchEvent。但是此时view mFirstTouchTarget已经为null了,所以下一次move来的时候,走的是case2,View:dispatchTouchEvent.这一点我之前是理解错误的。其实这里损失了一个MOVE事件,这个MOVE事件虽然返回了true,但是其实没有任何人处理他。
case2源码分析
由于case2和case3的代码我不太熟悉,所以抓出来分析一下。
先看case2,此时mFirstTouchTarget非空,在L13把事件发给child
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//在这里把事件发给child
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
case3源码分析
再来看case3,mFirstTouchTarget非空,intecepted为true
来看这段代码,L12因为intercepted为true,所以cancelChild为true,会走到dispatchTransformedTouchEvent,dispatchTransformedTouchEvent内部会发一个cancel事件出去,然后返回true(后边会详细说)。然后L18,因为cancelChild为null,所以会执行L21,把mFirstTouchTarget置null。
{
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//注意这里,因为intercepted为true,所以cancelChild也会为true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
//mFirstTouchTarget置null
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
我们再看看dispatchTransformedTouchEvent的流程,此时传进来的cancel为true,会再9设置CANCEL事件,在L14由child发出去。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//走这里,发一个cancel消息出去
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
//返回true
return handled;
}
UP的事件分发
UP事件其实和MOVE事件基本一致,UP事件一般不拦截。即使拦截了UP事件,也不会调用自己的View:dispatchTouchEvent.为什么?可以参考 move事件第二阶段的case3,简单来说如果拦截UP事件,此时mFirstTouchTarget非空的话,此次dispatchTouchEvent会让child发一个cancel出去,把自己的mFirstTouchTarget置空,然后返回true,不会调用View:dispatchTouchEvent。因为只有下一个事件来临的时候才调用View:dispatchTouchEvent,可是UP已经是最后一个事件了,所以不会发生后面的事。
伪代码
讲了这么多,我尝试着用伪代码写ViewGroup的dispatchTouchEvent,其实也不麻烦,40行代码说明了一切。
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
//查找合适的子view
if(down事件&&!intercepted){
handled=某个child.dispatchTouchEvent();
if(handled){
mFirstTouchTarget赋值
}
}
if(mFirstTouchTarget==null){
//只有这种情况,parent亲自处理
handled=super.dispatchTouchEvent()
}else{
if(intercepted){
mFirstTouchTarget发一个cancel事件
mFirstTouchTarget=null;
}else{
return mFirstTouchTarget.dispatchTouchEvent();
}
}
return handled;
问题答案
1、如果拦截了某个事件,是否就会交由本view的View:dispatchTouchEvent处理?
这里的拦截的意思是指在onInterceptTouchEvent里返回了true,如果拦截的只是down事件,那么必然会交给View:dispatchTouchEvent处理。如果拦截的只是MOVE事件,那么是不会交给View:dispatchTouchEvent处理的,此时只是把mFirstTouchTarget置null,下一个MOVE才会交由View:dispatchTouchEvent处理。如果拦截的只是UP事件,那就更加不可能交给View:dispatchTouchEvent处理了。
2、一个事件,如果子view处理失败,是否就交还给父view处理?
只有mFirstTouchTarget为null,才交由parent处理。down事件肯定会给parent处理,其他就不一定了,还是看mFirstTouchTarget的值。
3、如果一个down事件,大家都不处理,会怎么样?
这个文中说的很详细了,不停往上抛直到DecorView,返回false,然后MOVE和UP给了DecorView处理,DecorView拦下来返回false。相当于所有事件都丢弃了。
4、parent把事件传递给哪个子view呢?是根据位置查一遍的吗?
down是根据位置查的,move和up是根据mFirstTouchTarget来处理的
5、假设子view为c,c的parent为p,p的parent为pp。c成功处理了down事件,所以p的mFirstTouchTarget指向c,在p的dispatchTouchEvent过程里,c处理move失败,参考MOVE第二阶段的流程图,可以知道p的dispatchTouchEvent返回false,然后接着pp的dispatchTouchEvent也返回false,直到DecorView,这个和第三个问题有点像
总结
本文提了3条规则,画了三幅流程图,写了一段伪代码,希望以后我遇到事件分发的问题,都能从这里找到答案。