事件的分发在 View 中和 ViewGroup 中有所不同,分两部分进行分析
1. View 中的事件分发
View 的事件分发都从 dispatchTouchEvent()
开始,其逻辑相对简单,关键代码只有以下几行:
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
ListenerInfo
保存了各种touch,click,longclick 等listener,result = true 代表该view消费本次事件,可以看到只有两种情况:
1. 该 View 是 enabled 且 OnTouchListener.onTouch() 返回 ture
2. 若1不成立,再判断 View 的 onTouchEvent() 是否返回 ture
OnTouchListener.onTouch() 取决于我们自己的实现,而 onTouchEvent() 是有默认实现的,代码比较长,下面提炼出关键部分,其余省略
public boolean onTouchEvent(MotionEvent event) {
if ((viewFlags & ENABLED_MASK) == DISABLED) {
...
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
...
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
break;
...
}
return true;
}
return false;
}
可以看到return 语句有四处,按照以下的顺序进行判断:
- 如果 View 被设为 disable 的,但只要这个 View 是 CLICKABLE 、LONG_CLICKABLE 或 CONTEXT_CLICKABLE,那么依然消耗这个事件,直接返回true。可见,View 是否 enable,与是否消耗点击事件没有必然联系,只与界面显示以及是否触发 Listener 有关;
- 如果设置了 TouchDelegate,那么返回值完全由 TouchDelegate.onTouchEvent() 决定;
- 如果这个 View 是 CLICKABLE 、 LONG_CLICKABLE 或 CONTEXT_CLICKABLE 的,那么这里有两个关键点:
- MotionEvent.ACTION_UP 时会执行 performClick(),也即调用 OnClickListener.onClick()
- 一定会返回 true
- 如果以上条件都没有满足,返回 false 即不消耗事件
View 的事件分发还是比较简单的,总结一下:
- 如果 View 是 CLICKABLE 、 LONG_CLICKABLE 或 CONTEXT_CLICKABLE,不管它是否是 enable,那么 dispatchTouchEvent 一定会返回 ture,即一定会消耗事件
- 如果一个 View 被设为 disable,那么它的 onTouch(),onClick(), onLongClick() 都不会被调用
- 方法调用顺序 OnTouchListener.onTouch -> onTouchEvent -> ClickListener。onTouch() 返回了true,onTouchEvent() 就不会再被调用
2. ViewGroup 中的事件分发
ViewGroup 中可能包含多个子 View,其事件的分发逻辑自然就复杂一些,不过事件依然是从 dispatchTouchEvent()
开始分发。这个函数代码比较长,下面挑出关键代码,分段来看
public boolean dispatchTouchEvent(MotionEvent ev) {
//代码片段1
...
// 如果是 DOWN 事件,清空之前的状态和事件的消费者
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 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);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
...
}
- 首先判断是否是 DOWN 事件,因为这意味着新一轮事件分发的开始,如果是 DOWN 事件,之前的保存的状态都需要重置(详见
cancelAndClearTouchTargets()
和resetTouchState()
方法) - 接着要判断该 ViewGroup 是否要拦截事件,拦截事件意味着其子 View 都不会收到该事件,这里的判断逻辑比较复杂。首先需要搞清几个变量的作用:
- mFirstTouchTarget,保存能够处理事件的子View,初始为 null,它其实是一个由 TouchTarget 组成的链表的头指针(为什么要用链表?一个事件序列应该最多只有一个目标View吧?没想明白...)。当 DOWN 事件到来时,也会清空链表并将该变量置为 null。为这个链表添加节点的操作在后面讲解;
- FLAG_DISALLOW_INTERCEPT,该标志位标示子 View 是否允许其父ViewGroup 拦截事件,子 View 可以通过调用
requestDisallowInterceptTouchEvent()
来设置该标志位。但是在1.中已经提到,每当 DOWN 事件到来的时候会重置该标志位,所以当事件为 DOWN 时disallowIntercept
值为 false
-
disallowIntercept
为 false 就会进入下面的 if 判断,这里就会调用onInterceptTouchEvent()
方法,这个方法默认返回 false,我们需要按需自己进行实现。从方法名也可看出这个函数是用来判断该 ViewGroup 是否拦截事件的,返回 ture 时代表拦截,反之则不拦截。 - 如果在 DOWN 事件时,
onInterceptTouchEvent()
返回 true,即 ViewGroup 选择拦截事件 :
public boolean dispatchTouchEvent(MotionEvent ev) {
//代码片段2
if (!canceled && !intercepted) {
...
}
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else {
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
handled = true;
}
}
}// end while
...
}
}
此时第一个 if 中的代码都不会被执行,第二个 if 中 mFirstTouchTarget == null
条件成立,因为此时还没有任何可以处理事件的子View,所以就会执行 dispatchTransformedTouchEvent()
方法,注意到传入的第三个参数,也就是 child = null(见下面代码),所以最终调用 super.dispatchTouchEvent()
,也就是在第一节中分析的 View.dispatchTouchEvent()
方法,也即 ViewGroup 自己拦截并处理了事件。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
...
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
...
接下来的 MOVE 和 UP 事件到来时,又一次进入代码片段1,此时第二个 if 中actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
为 false,所以 onInterceptTouchEvent() 方法不会再调用,直接执行 intercepted = true,接下来都和 DOWN 事件一样了。所以 一旦VIewGroup 在 DOWN 事件进行了拦截,接下来的事件都会直接交给 ViewGroup 处理,子View也无法通过requestDisallowInterceptTouchEvent() 阻止 ViewGroup 拦截
- 如果在 DOWN 事件时,onInterceptTouchEvent() 返回 false ,此时代码片段3中的代码会执行:
public boolean dispatchTouchEvent(MotionEvent ev) {
//代码片段3
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
for (int i = childrenCount - 1; i >= 0; i--) {
...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
...
}//end for
}
}
}
这主要逻辑是在 for 循环中遍历子View,如果 dispatchTransformedTouchEvent()
方法返回 true,代表当前遍历的子View消耗事件(即子View的 dispatchTouchEvent()
返回 ture,参见上面的 dispatchTransformedTouchEvent()
代码),则将其添加到 mFirstTouchTarget 指向的链表中(具体操作在 addTouchTarget()),然后直接break跳出循环。
回到代码片段2继续往下执行,如果前面找到了处理事件的子View,则 mFirstTouchTarget == null
为 false,else里的代码会被执行,主要逻辑就是遍历 mFirstTouchTarget 指向的链表里,并调用 dispatchTransformedTouchEvent()
把事件分发给子View。
之后的 MOVE 和 UP 事件到来时,又一次进入代码片段1,此时 mFirstTouchTarget != null
,所以 actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
为true,每次都会调用到 onInterceptTouchEvent(),所以 ViewGroup 都还有机会拦截事件。一旦ViewGroup 拦截了事件,子 View 都会立刻收到 CANCEL 事件,原因是代码片段2中 cancelChild = resetCancelNextUpFlag(target.child) || intercepted
, cancelChild = true,传入 dispatchTransformedTouchEvent() 的第二个参数,就会导致子View收到 CANCEL 事件。
ViewGroup 的事件总结如下:
- 如果在 DOWN 事件就拦截,即 onInterceptTouchEvent() 返回 true,那么之后onInterceptTouchEvent() 不会再调用,所有的后续事件都会直接交给 ViewGroup 处理
- 如果在 DOWN 事件不拦截,那么后续所有的事件到来时,都会调用 onInterceptTouchEvent(),来询问 ViewGroup 是否要拦截,一旦 ViewGroup 决定拦截,之前接收事件的子 View 都会收到 CANCEL 事件。拦截的事件都会交给 ViewGroup 的 dispatchTouchEvent() 处理,之后的逻辑就是第一部分讲的 View 的事件分发逻辑
- 如果子View在 DOWN 事件的时候选择不消耗事件,那么后续的事件都不会再传给子View,子View也没有机会再获取到事件,即使调用
requestDisallowInterceptTouchEvent()
也没用。