Android 事件分发机制三:事件分发工作流程
该方法只存在于 viewGroup 中,当一个事件需要被分发到子 view 时,viewGroup 会调用此方法检查是否要进行拦截。如果拦截则自己处理,而如果不拦截才会调用子 view 的?dispatchTouchEvent
?方法分发事件。
方法返回 true 表示拦截事件,返回 false 表示不拦截。
这个方法默认只对鼠标的相关操作的一种特殊情况进行了拦截,其他的情况需要具体的实现类去重写拦截。
onTouchEvent
该方法是消费事件的主要方法,存在于 view 中,viewGroup 默认并没有重写该方法。方法返回 true 表示消费事件,返回 false 表示不消费事件。
viewGroup 分发事件时,如果没有一个子 view 消费事件,那么会调用自身的 onTouchEvent 方法来处理事件。View 的 dispatchTouchEvent 方法中,并不是直接调用 onTouchEvent 方法来消费事件,而是先调用 onTouchListener 判断是否消费;如果 onTouchListener 没有消费事件,才会调用 onTouchEvent 来处理事件。
我们为 view 设置的 onClickListener 与 onLongClickListener 都是在 View 的 dispatchTouchEvent 方法中,根据具体的触摸情况被调用。
重要规则
事件分发有一个很重要的原则:一个触控点的事件序列只能给一个 view 消费,除非发生异常情况如被 viewGroup 拦截?。具体到代码实现就是:消费了一个触控点事件序列的 down 事件的 view,将持续消费该触控点事件序列接下来的所有的事件?。举个栗子:
当我手指按下屏幕时产生了一个 down 事件,只有一个 view 消费了这个 down 事件,那么接下来我的手指滑动屏幕产生的 move 事件会且仅会给这个 view 消费。而当我手机抬起,再按下时,这时候又会产生新的 down 事件,那么这个时候就会再一次去寻找消费 down 事件的 view。所以,事件分发,是以事件序列为单位的?。
因此下面的工作流程中都是指 down 事件的分发?,而不是 ACTION_MOVE 或 ACTION_UP 的分发。因为消费了 down 事件,意味着接下来的 move 和 up 事件都会给这个 view 处理,也就无所谓分发了。但同时注意事件序列是可以被 viewGroup 的 onInterceptTouchEvent 中断的,这些就属于其他的情况了。
细心的读者还会发现事件分发中包含了多点触控。在多点触控的情况下,ACTION_POINTER_DOWN 与 ACTION_DOWN 的分发规则是不同的,具体可前往第二篇文章了解详细。ACTION_POINTER_DOWN 在 ACTION_DOWN 的分发模型上稍作了一些修改而已,后面会详细解析,
工作流程模型
工作流程模型,本质上就是不同的控件对象,viewGroup 和 view 之间事件分发方法的关系。需要注意的是,这里讨论的是 viewGroup 和 view 的默认方法实现,不涉及其他实现类如 DecorView 的重写方法。
下面用一段伪代码来表示三个事件分发方法之间的关系(?这里再次强调,这里的事件分发模型分发的事件类型是 ACTION_DOWN 且都是默认的方法,没有经过重写,这点很重要?):
public boolean dispatchTouchEvent(MotionEvent event){
// 先判断是否拦截
if (onInterceptTouchEvent()){
// 如果拦截调用自身的 onTouchEvent 方法判断是否消费事件
return onTouchEvent(event);
}
// 否则调用子 view 的分发方法判断是否处理事件
if (childView.dispatchTouchEvent(event)){
return true;
}else{
return onTouchEvent(event);
}
}
这段代码非常好的展示了三个方法之间的关系:在 viewGroup 收到触摸事件时,会先去调用?onInterceptTouchEvent
?方法判断是否拦截,如果拦截则调用自己的?onTouchEvent
?方法处理事件,否则调用子 view 的?dispatchTouchEvent
?方法来分发事件。因为子 view 也有可能是一个 viewGroup,这样就形成了一个类似递归的关系。
这里我再补上 view 分发逻辑的简化伪代码:
public boolean dispatchTo
uchEvent(MotionEvent event){
// 先判断是否存在 onTouchListener 且返回值为 true
if (mOnTouchListener!=null && mOnTouchListener.onTouch(event)){
// 如果成功消费则返回 true
return true;
}else{
// 否则调用 onTouchEvent 消费事件
return onTouchEvent(event);
}
}
view 与 viewGroup 不同的是他不需要分发事件,所以也就没有必要拦截事件。view 会先检查是否有 onTouchListener 且返回值是否为 true,如果是 true 则直接返回,否则调用 onTouchEvent 方法来处理事件。
基于上述的关系,可以得到下面的工作流程图:
这里为了展示类递归关系使用了画了两个 viewGroup,只需看中间一个即可,下面对这个图进行解析:
viewGroup
viewGroup 的 dispatchTouchEvent 方法接收到事件消息,首先会去调用 onInterceptTouchEvent 判断是否拦截事件
如果拦截,则调用自身的 onTouchEvent 方法
如果不拦截则调用子 view 的 dispatchTouchEvent 方法
子 view 没有消费事件,那么会调用 viewGroup 本身的 onTouchEvent
上面 1、2 步的处理结果为 viewGroup 的 dispatchTouchEvent 方法的处理结果,并返回给上一层的 onTouchEvent 处理
view
view 的 dispatchTouchEvent 默认情况下会调用 onTouchEvent 来处理事件,返回 true 表示消费事件,返回 false 表示没有消费事件
第 1 步的结果就是 dispatchTouchEvent 方法的处理结果,成功消费则返回 true,没有消费则返回 false 并交给上一层的 onTouchEvent 处理
可以看到整个工作流程就是一个“U”型结构,在不拦截的情况下,会一层层向下寻找消费事件的 view。而如果当前 view 不处理事件,那么就一层层向上抛,寻找处理的 viewGroup。
上述的工作流程模型并不是完整的,还有其他的特殊情况没有考虑。下面讨论几种特殊的情况:
事件序列被中断
我们知道,当一个 view 接收了 down 事件之后,该触控点接下来的事件都会被这个 view 消费。但是,viewGroup 是可以在中途掐断事件流的,因为每一个需要分发给子 view 的事件都需要经过拦截方法:onInterceptTouchEvent
?(当然,这里不讨论子 view 设置不拦截标志的情况)。那么当 viewGroup 掐断事件流之后,事件的走向又是如何的呢?参看下图:(注意,这里不讨论多指操作的情况,仅讨论单指操作的 move 或 up 事件被 viewGroup 拦截的情况)
当 viewGroup 拦截子 view 的 move 或 up 事件之后,会将当前事件改为 cancel 事件并发送给子 view
如果当前事件序列还未结束,那些接下来的事件都会交给 viewGroup 的 ouTouchEvent 处理
此时不管是 viewGroup 还是 view 的 onTouchEvent 返回了 false,那么将导致整个控件树的 dispatchTouchEvent 方法返回 false
秉承着一个事件序列只能给一个 view 消费的原则,如果一个 view 消耗了 down 事件却在接下来的 move 或 up 事件返回了 false,那么此事件不会给上层的 viewGroup 处理,而是直接返回 false。
多点触控情况
上面讨论的所有情况,都是不包含多点触控情况的。多点触控的情况,在原有的事件分发流程上,新增了一些特殊情况。这里就不再画图,而是把一些特殊情况描述一下,读者了解一下就可以了。
默认情况下,viewGroup 是支持多点触控的分发,但 view 是不支持多点触控的,需要自己去重写?dispatchTouchEvent
?方法来支持多点触控。
多点触控的分发规则如下:
viewGroup 在已有 view 接受了其他触点的 down 事件的情况下,另一个手指按下产生 ACTION_POINTER_DOWN 事件传递给 viewGroup:
viewGroup 会按照 ACTION_DOWN 的方式去分发 ACTION_POINTER_DOWN 事件
如果子 view 消费该事件,那么和单点触控的流程一致
如果子 view 未消费该事件,那么会交给上一个最后接收 down 事件的 view 去处理
viewGroup 两个 view 接收了不同的 down 事件,那么拦截其中一个 view 的事件序列,viewGroup 不会消费拦截的事件序列。换句话说,viewGroup 和其中的 view 不能同时接收触摸事件。
Activity 的事件分发
细心的读者会发现,上述的工作流程并不涉及 Activity。我们印象中的事件分发都是?Activity -> Window -> ViewGroup
?,那么这是怎么回事?这一切,都是 DecorView “惹的祸” 。
DecorView 重写 viewGroup 的?dispatchTouchEvent
?方法,当接收到触摸事件后,DecorView 会首先把触摸对象传递给内部的 callBack 对象。没错,这个 callBack 对象就是 Activity。加入 Activity 这个环节之后,分发的流程如下图所示:
整体上和前面的流程没有多大的不同,Activity 继承了 Window.CallBack 接口,所以也有 dispatchTouchEvent 和 onTouchEvent 方法。对上图做个简单的分析:
activity 接收到触摸事件之后,会直接把触摸事件分发给 viewGroup
如果 viewGroup 的 dispatchTouchEvent 方法返回 false,那么会调用 Activity 的 onTouchEvent 来处理事件
第 1、2 步的处理结果就是 activity 的 dispatchTouchEvent 方法的处理结果,并返回给上层
上面的流程不仅适用于 Activity,同样适用于 Dialog 等使用 DecorView 和 callback 模式的控件系统。
总结
--
到这里,事件分发的主要内容也就讲解完了。结合前两篇文章,相信读者对于事件分发有更高的认知。
纸上得来终觉浅,绝知此事要躬行。学了知识之后最重要的就是实践。下一篇文章将简单分析一下如何利用学习到的事件分发知识运用到实际开发中。
最后
评论