原文:http://xiazdong.me/2015/09/19/touch-dispatch-mechanism/
前言
Touch 事件分发机制是面试中非常常见的问题,也是非常重要的问题。网上有很多关于这方面的文章,但是感觉写的不是特别清晰易懂。
基本概念
Touch 事件分发机制分发的是 MotionEvent 对象,取值如下:
- ACTION_DOWN: 按下事件。
- ACTION_MOVE: 移动事件。
- ACTION_UP: 抬起事件。
一个事件序列包含 ACTION_DOWN->ACTION_MOVE->...->ACTION_MOVE->ACTION_UP
,即用户触摸屏幕,移动一些距离,然后抬起。
- 下面说的 view "处理" 了某个事件,表示 view 调用了 onTouchEvent()。
- 下面说的 view "消费" 了某个事件,表示 view 调用了 onTouchEvent() 并返回 true。因此某个 view 可以处理但不消费某个事件。
Touch 事件分发机制涉及三个方法:
-
dispatchTouchEvent(MotionEvent ev)
: 如果某个触摸事件传递给了某个 View 或 ViewGroup(设为 v),则一定会调用 v.dispatchTouchEvent(),如果 v 是 ViewGroup,则内部会调用 onInterceptTouchEvent() 或 onTouchEvent();如果 v 是 View,则内部会调用 onTouchEvent()。 -
onInterceptTouchEvent(MotionEvent ev)
: 这个方法只有 ViewGroup 才有,判断是否要拦截该事件并且自己处理,如果返回 true,则拦截;如果返回 false,则不拦截。 -
onTouchEvent(MotionEvent ev)
: 处理 Touch 事件的核心方法,如果返回 true,表示消费了事件;如果返回 false,则表示没消费该事件。
ViewGroup 的 dispatchTouchEvent(), onInterceptTouchEvent(), onTouchEvent() 的基本关系如下:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){ //是否拦截
consume = onTouchEvent(ev); //如果拦截,则自己处理
}
else{ //如果没拦截,则事件分发给孩子
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
上面的代码只是基本概括了整个事件分发的核心流程,具体实现细节会在下面介绍。
总体分发流程
当用户发起触摸事件后,首先触摸事件从 Activity 的 dispatchTouchEvent() 开始,该方法实现如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
解释:
- 其中 getWindow().superDispatchTouchEvent() 会调用 DecorView 的 dispatchTouchEvent() 开始分发,接着 DecorView 会调用根 View 进行事件分发。
- 如果 getWindow().superDispatchTouchEvent() 返回 true,表示有子 View 消费了该触摸事件;如果返回 false,表示没有任何子 View 消费该事件,会调用 Activity 的 onTouchEvent(),即 Activity 自己处理 Touch 事件,并返回 false。
View Touch 事件分发
因为 ViewGroup 也是继承自 View,因此此处分两种情况讨论。
- 如果是最底层的 View,一旦将触摸事件分发给他,就会调用下面的 dispatchTouchEvent();
- 如果是 ViewGroup,则默认并不会调用下面的 dispatchTouchEvent(),而是会调用 ViewGroup 自己的 dispatchTouchEvent(),只有当 ViewGroup 在自己的 dispatchTouchEvent() 方法中经过判断,发现需要自己处理触摸事件时,才会通过
super.dispatchTouchEvent(ev)
的形式调用下面的 dispatchTouchEvent()。
View 的 dispatchTouchEvent() 实现如下:
public boolean dispatchTouchEvent(MotionEvent event)
{
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event))
{
return true;
}
if (onTouchEvent(event))
{
return true;
}
return false;
}
从上面看出:
- onTouchEvent() 不一定会被调用。如果设置了 OnTouchListener, View 是 enabled,并且 OnTouchListener 的 onTouch() 返回 true,则不会调用 onTouchEvent()。
- 如果 View 是 DISABLED,则 onTouch() 不会被调用。
- 如果 OnTouchListener 的 onTouch() 或 View 的 onTouchEvent() 返回 true,则 dispatchTouchEvent() 返回 true;否则返回 false。
接着我们看看 onTouchEvent() 的实现:
public boolean onTouchEvent(MotionEvent event) {
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
performClick(); //执行 mClickListener.onClick() 方法
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
从上面可以看出:
- onTouchEvent() 内部在 ACTION_UP 事件中调用了 onClick()(即在一个事件序列中,只有在 ACTION_UP 才会调用 onClick(),但是 onTouch() 在每个事件都会被调用),即如果同时注册了 OnTouchListener 和 OnClickListener,则 OnTouchListener 优先级高于 OnClickListener,如果 onTouch() 返回 true,则 onTouchEvent() 不会执行,也就意味着 onClick() 不会执行。
- 在 onTouchEvent() 中,只有 View 是 Clickable 的,才能进入 if 语句,而且一旦进入 if 语句就返回 true。比如 Button 是 Clickable 的,因此 onTouchEvent() 一定返回 true,ImageView 是不可点击的,因此 onTouchEvent() 一定返回 false。
ViewGroup Touch 事件分发
ViewGroup 的 dispatchTouchEvent() 比较复杂,下面我们分析一下。
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
// 1、如果是 ACTION_DOWN 动作,则清除标志位。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState(); // 清除 FLAG_DISALLOW_INTERCEPT 标记位
}
// 2、判断是否要拦截该事件
final boolean intercepted;
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null){
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
}
else{
intercepted = true;
}
// 3、如果不拦截,则传给子 View
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN){
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = children[childIndex];
//如果该子 View 不在触摸范围内,则略过
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
//如果子 View 的 dispatchTouchEvent 返回 true,
// 则表示有子 View 处理了该事件,则设置 mFirstTouchTarget
//如果子 View 的 dispatchTouchEvent 返回 false,
// 则表示没有子 View 处理了该事件,则不设置 mFirstTouchTarget
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//该方法中设置了 mFirstTouchTarget = child
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
// 4、判断是否有子 View 处理了事件
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}
else {
if (alreadyDispatchedToNewTouchTarget){
handled = true;
}
else{
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
handled = true;
}
}
}
// 5、如果是一个事件序列的最后一个操作(ACTION_UP 或 ACTION_CANCEL),则把状态清空
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
}
}
上面的代码中多处调用了 dispatchTransformedTouchEvent(),其中第三个参数有 null(第 52 行) 或者 child(第 40 行、第 59 行)。这个方法的实现如下:
public boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits){
boolean handled = false;
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
return handled;
}
从上面代码可以看出,如果第三个参数传入 null,则调用 View 的 dispatchTouchEvent() 即自己处理事件;如果第三个参数传入 child,则将事件分发下去,即调用 child.dispatchTouchEvent()。
解释:
- 第 2 行: handled 变量作为 dispatchTouchEvent() 的返回值。
- 第 4-7 行: 如果是 ACTION_DOWN 操作,则将其状态清空,包括 FLAG_DISALLOW_INTERCEPT。我们可以通过 requestDisallowInterceptTouchEvent() 将 FLAG_DISALLOW_INTERCEPT 设置为 true,表示该 ViewGroup 禁止拦截操作(即直接将 intercepted 设为 false,不调用 onInterceptTouchEvent()),但是这个设置对于 ACTION_DOWN 无效,因为第 4-7 行会将该状态清除,即使设置了该状态,ACTION_DOWN 操作还是会调用 onInterceptTouchEvent()。
- 第 10-22 行: 每个 ViewGroup 都会带有 mFirstTouchTarget 变量,这个变量只有在 ACTION_DOWN 事件时才能设置,这个能从第 28 行的 if 语句看出来,因为设置 mFirstTouchTarget 是在第 40-45 行(只有第 28 行的 if 语句为 true 才能执行第 40-45 行代码),可以看出如果有某个子 View 消费了该事件(这里不一定是直接子 View 消费了该事件,比如有 View 关系: v1->v2->v3,当前调用了 v1.dispatchTouchEvent(),如果 v3 消费了该事件,则表示 v1 的某个子 View 消费了该事件,并将 v1 的 mFirstTouchTarget 设置为 v2,将 v2 的 mFirstTouchTarget 设置为 v3),这样才能使第 40 行的 dispatchTransformedTouchEvent() 返回 true,并设置 mFirstTouchTarget。
- 第 25-48 行: 如果当前是 ACTION_DOWN 事件(第 28 行)并且没有拦截(第 27 行,onInterceptTouchEvent() 返回 false),则会将 ACTION_DOWN 事件分发给合适的在触摸点的直接子 View,在第 40 行的 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) 中会调用 child 的 dispatchTouchEvent()。如果 child.dispatchTouchEvent() 返回 false,表示 child 或 child 的子 View 没有人消费 ACTION_DOWN 事件,这样并不会给当前 ViewGroup 设置 mFirstTouchTarget。如果 child.dispatchTouchEvent() 返回 true,即表示 child 或 child 的子 View 有人消费 ACTION_DOWN 事件,则为当前 ViewGroup 设置 mFirstTouchTarget = child,并设置 alreadyDispatchedToNewTouchTarget = true,这个变量在后面会用到,表示是不是在这个方法中刚刚设置了 mFirstTouchTarget,因为只有 ACTION_DOWN 操作才能设置 mFirstTouchTarget,如果
alreadyDispatchedToNewTouchTarget == true && mFirstTouchTarget != null
,则表示当前是 ACTION_DOWN 事件并且在该方法中刚刚设置了 mFirstTouchTarget;如果alreadyDispatchedToNewTouchTarget == false && mFirstTouchTarget != null
,则表示 mFirstTouchTarget 并不是该方法中刚刚设置的,即当前不是 ACTION_DOWN 事件。 - 第 51-53 行: 有两种情况会进入第 51 行的 if 语句,(1)当前是 ACTION_DOWN 操作,并且没有子 View 处理该事件 (2)当前不是 ACTION_DOWN 操作,并且在前面的 ACTION_DOWN 操作时没有子 View 处理该事件。第 52 行的第三个参数为 null,因此会执行
super.dispatchTouchEvent()
,即执行 View 的 dispatchTouchEvent(),表示自己处理该事件。 - 第 55-57 行: 因为只有当当前为 ACTION_DOWN 操作并且有子 View 处理了该事件时,alreadyDispatchedToNewTouchTarget 才为 true,这时直接将 handled 设为 true,不需要做额外的操作。
- 第 59-61 行: 能够进入第 58 行的 else 语句意味着当前事件不是 ACTION_DOWN 并且在前面的 ACTION_DOWN 事件存在子 View 处理了该事件(即 mFirstTouchTarget != null,即在前面的 ACTION_DOWN 事件执行过第 40-46 行代码)。此时就执行 dispatchTransformedTouchEvent(),内部会调用 mFirstTouchTarget.dispatchTouchEvent()。
- 第 66-69 行: 做收尾工作。
一些结论的验证
在网上有很多关于 Touch 事件的结论,这些结论其实都可以通过分析上面的代码得出。这里举几个例子:
- "某个 view 一旦开始处理事件,如果不消费 ACTION_DOWN 事件,则同一事件序列中的其他事件不会再交给它来处理": 如果 view 处理但不消费 ACTION_DOWN 事件,则表示执行了第 40 行代码,但是返回 false(这个 view 作为 child 传入),设这个 view 的父 view 为 v0,此时就没设置 v0 的 mFirstTouchTarget。因此接下来的事件(比如 ACTION_MOVE) 一旦分发到 v0,因为他的 mFirstTouchTarget == null,因此会执行第 52 行代码,即 v0 自己处理该事件。
- "某个 view 一旦决定拦截,那么这一事件序列都只能由它来处理,并且它的 onInterceptTouchEvent 不会被调用": 因为 view 拦截了事件,因此第 27 行的 if 语句进不去,也就设置不了 view 的 mFirstTouchTarget,接下来的事件分发给 view 时,执行到第 11 行的 if 语句,因为该事件不是 ACTION_DOWN 并且 view 的 mFirstTouchTarget == null,因此 if 语句返回 false,即执行第 21 行设置 intercepted = true,因此不会调用 onInterceptTouchEvent;接着执行到第 51 行,因为 view 的 mFirstTouchTarget 为 null,因此执行第 52 行代码,即自己处理该事件。
网友评论