事件分发源代码分析
1. Activity 事件分发
首先从 Activity 的 dispatchTouchEvent 方法入手
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可以看出,Activity 其实是调用了 Window 的 superDispatchTouchEvent 方法,而 Window 的实现类是 PhoneWindow,因此我们直接查看 PhoneWindow 的 superDispatchTouchEvent 方法
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
发现是直接调用的 DecorView 的 superDispatchTouchEvent 方法,再进一步查看
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
原来这儿就调用了 ViewGroup 的 dispatchTouchEvent 方法,也就是说界面上的事件直接传递给了根布局的 dispatchTouchEvent 方法
2. ViewGroup 事件分发
在讲 ViewGroup 的 dispatchTouchEvent 方法之前,我们先看看 ViewGroup 的 dispatchTransformedTouchEvent 方法,dispatchTouchEvent 内部多次调用了 dispatchTransformedTouchEvent 方法。因为代码量较多,这里只提取我们关心的部分
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 如果是取消操作,则直接分发取消事件
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
// 如果传入的 child 不为空,则调用 child 的 dispatchTouchEvent 方法,否则调用自身的 dispatchTouchEvent 方法
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
......
// 如果传入的 child 不为空,则调用 child 的 dispatchTouchEvent 方法,否则调用自身的 dispatchTouchEvent 方法
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
......
handled = child.dispatchTouchEvent(transformedEvent);
}
......
return handled;
}
可以看出 dispatchTransformedTouchEvent 方法主要做了两件事
- 如果传入的事件是 ACTION_CANCEL,或者 cancel 参数为 true,则直接分发 ACTION_CANCEL 事件
- 分发过程中,如果 child 为空,则调用当前 View 的 super.dispatchTouchEvent 方法,这是因为 ViewGroup 的 dispatchTouchEvent 方法会被重写,而此时调用 super 的方法也就是调用 View 的 dispatchTouchEvent 方法;如果 child 不为空,则调用这个子 View 的 dispatchTouchEvent 方法。
然后我们再来看看 dispatchTouchEvent 方法,同样代码量特别多,我们只抽取我们关心的,即使这样代码量依然很多。我们先列出来,不用仔细看,后面会分块拆分讲解
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 1. DOWN 事件进行初始化,清空 TouchTargets 和 TouchState
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 2. 检查是否拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 是否强制不允许拦截,子 View 可以设置 parent 强制不允许拦截,默认为 false
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 {
intercepted = true;
}
......
// 3. 如果没有被拦截, 先处理 DOWN 事件,主要是赋值 TouchTarget
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
......
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
......
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
......
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
......
// 找到 Visible 并且处于点击范围的子 View
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
......
// 相当于调用子 View 的 dispatchTouchEvent 方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
......
// 赋值 TouchTarget,刷新标志位
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
......
}
......
}
......
}
}
// 4. 是自己处理事件还是交由子 View 处理事件
if (mFirstTouchTarget == null) {
// 没有子 View 消耗事件,则自己消耗,相当于调用 super.dispatchTouchEvent 方法
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 如果是 DOWN 事件,则上面已经调用了子 View 的 dispatchTouchEvent 方法,则什么都不用做
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 根据 intercepted 决定是否将事件强制改为 CANCEL 事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 相当于调用子 View 的 dispatchTouchEvent 方法。如果 intercepted=true,此时会强制将 action 改为 CANCEL;如果 intercepted=false,则
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 如果 intercepted=true,则将 mFirstTouchTarget 置为 null
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
......
}
......
return handled;
}
dispatchTouchEvent 方法主要由4个模块组成的
- DOWN 事件进行初始化,清空 TouchTargets 和 TouchState
- 检查是否拦截
- 如果没有被拦截, 先处理 DOWN 事件,主要是赋值 TouchTarget
- 是自己处理事件还是交由子 View 处理事件
A. 第一步:DOWN 事件时进行初始化
// 1. DOWN 事件进行初始化,清空 TouchTargets 和 TouchState
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
我们知道,一个事件序列是由一个 ACTION_DOWN,零个或多个 ACTION_MOVE 和一个 ACTION_UP 组成的。ACTION_DOWN 是一个事件序列的开始,ACTION_UP 是一个事件序列的结束。在 ACTION_DOWN 时,需要对一些状态进行初始化和重置。上面的 cancelAndClearTouchTargets 和 resetTouchState 方法,都是初始化一些状态,最重要的我们关心的就是内部调用了这个初始化代码
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
可以看出来这里是用循环的方式把单链表 mFirstTouchTarget 给清空了,至于 mFirstTouchTarget 是什么这里先不讲,只需要知道它保存了一个指定消耗事件的子 View 便可,后续的所有事件会直接交给这个 View 消耗。
B. 第二步:检查是否拦截
// 2. 检查是否拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 是否强制不允许拦截,子 View 可以设置 parent 强制不允许拦截,默认为 false
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 {
intercepted = true;
}
这里我们可以看出,如果是 ACTION_DOWN 事件,或者 mFirstTouchTarget 不为空的时候,就会进入是否拦截的逻辑判断。这里有一个 disallowIntercept 的判断,这个标记是子 View 设置 ViewGroup 是否允许拦截的情况,这里我们暂不用理解这种情况,默认 disallowIntercept 为 false,也就是允许拦截。此时会判断 onInterceptTouchEvent 方法,而 onInterceptTouchEvent 方法默认返回 false。
而 mFirstTouchTarget 是怎么来的呢?在后面会有详细的讲解,这里先简单的说明一下:
- 如果 intercepted 为 false,并且在后面有一个子 View 的 dispatchTouchEvent 方法在 ACTION_DOWN 时返回了 true,那么就会对 mFirstTouchTarget 进行赋值;
- 如果 intercepted 为 true,那么在后面就会对 mFirstTouchTarget 置为 null
我们来走一遍所有可能的路径:
- 事件序列开始,也就是说,在 ACTION_DOWN 时,会调用 onInterceptTouchEvent 判断是否拦截;
- 如果此时 onInterceptTouchEvent 返回 true,intercepted 赋值为 true,则后面 mFirstTouchTarget 不会再被赋值,mFirstTouchTarget 会一直为 null,后续的 ACTION_MOVE 和 ACTION_UP 事件 onInterceptTouchEvent 方法不会再被调用,intercepted 会一直为 true
- 如果此时 onInterceptTouchEvent 返回 false,intercepted 赋值为 false,后面如果有子 View 的 dispatchTouchEvent 方法返回了 true,mFirstTouchTarget 会被赋值,后续的 ACTION_MOVE 和 ACTION_UP 事件每次都会调用 onInterceptTouchEvent 方法
- 在后续的 ACTION_MOVE 和 ACTION_UP 事件中,如果 onInterceptTouchEvent 返回了 true,那么在后面会清空 mFirstTouchTarget 的值,那么在下一个事件直到最后一个事件,onInterceptTouchEvent 方法又不会被调用了
总结来说,onInterceptTouchEvent 一旦某一次返回了 true,那么后面的事件都不会再调用 onInterceptTouchEvent 进行是否拦截的判断,intercepted 的值会一直为 true
C. 第三步:先处理 DOWN 事件
// 3. 如果没有被拦截, 先处理 DOWN 事件,主要是赋值 TouchTarget
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
......
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
......
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
......
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
......
// 找到 Visible 并且处于点击范围的子 View
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
......
// 相当于调用子 View 的 dispatchTouchEvent 方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
......
// 赋值 TouchTarget,刷新标志位
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
......
}
......
}
......
}
}
这段代码的前提条件就是 intercepted 为 false,并且是 ACTION_DOWN 事件,此时会遍历每一个满足条件(处于Visible状态并且处于点击范围内)的子 View,调用了 dispatchTransformedTouchEvent 方法,dispatchTransformedTouchEvent 方法最开始我们就讲解过,此时的 child 参数不为空,所以就是调用的 child 的 dispatchTouchEvent 方法。如果 dispatchTransformedTouchEvent 返回为 true,则调用 addTouchTarget 方法对 mFirstTouchTarget 进行赋值,并且将变量 alreadyDispatchedToNewTouchTarget 置为 true,然后紧跟了一个 break 跳出循环,为什么要跳出循环呢?这是因为一个事件只能被一个 View 消耗(dispatchTouchEvent 返回 true 代表消耗)。
我们看看 addTouchTarget 方法
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
就是一个单链表操作,新增一个 TouchTarget 并插入到表头,然后将表头赋值给 mFirstTouchTarget。
总结来说,ACTION_DOWN 时,如果 intercepted 为 true,则不会有任何子 View 调用 dispatchTouchEvent 方法,并且 mFirstTouchTarget 不会被赋值,会一直为 null;如果 intercepted 为 false,那么在满足条件的某个子 View 的 dispatchTouchEvent 方法返回 true 时,mFirstTouchTarget 也会被赋值
D. 第四步:分发事件
// 4. 是自己处理事件还是交由子 View 处理事件
if (mFirstTouchTarget == null) {
// 没有子 View 消耗事件,则自己消耗,相当于调用 super.dispatchTouchEvent 方法
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 如果是 DOWN 事件,则上面已经调用了子 View 的 dispatchTouchEvent 方法,则什么都不用做
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 根据 intercepted 决定是否将事件强制改为 CANCEL 事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 相当于调用子 View 的 dispatchTouchEvent 方法。如果 intercepted=true,此时会强制将 action 改为 CANCEL;如果 intercepted=false,则
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 如果 intercepted=true,则将 mFirstTouchTarget 置为 null
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
可以看到,只要 mFirstTouchTarget 为 null,那么就会直接调用 dispatchTransformedTouchEvent 方法,child 参数为 null,那么就会调用当前 ViewGroup 的 super.dispatchTouchEvent 方法。
如果 mFirstTouchTarget 不为 null,首先判断 alreadyDispatchedToNewTouchTarget 和 newTouchTarget 变量决定是否直接返回。由上面第3步可知,如果 alreadyDispatchedToNewTouchTarget 为 true,则代表当前是 ACTION_DOWN 事件,并且找到了一个子 View 并且调用过了 child 的 dispatchTouchEvent 方法,因此这里什么都不用做。
然后再看看 cancelChild 变量,它是由 intercepted 决定的。假如 cancelChild 为 false,说明 intercepted 一定为 false,然后因为 mFirstTouchTarget 不为空,那么调用 dispatchTransformedTouchEvent 时 child 不为空,则直接调用 child 的 dispatchTouchEvent 方法;如果 intercepted 为 true,那么 cancelChild 一定为 true,dispatchTransformedTouchEvent 的 cancel 参数为 true,那么会直接给 mFirstTouchTarget 对应的 child 传递一个 ACTION_CANCEL 事件,然后在 mFirstTouchTarget 对应的单链表中,删除 mFirstTouchTarget 当前对应的 TouchTarget
总结一下,如果 mFirstTouchTarget 为 null,则调用自身的 super.dispatchTouchEvent 方法;如果 mFirstTouchTarget 不为 null,如果上面已经处理过 ACTION_DOWN 事件,则什么都不做;如果 intercepted 为 false,则直接调用 mFirstTouchTarget 对应的 child 的 dispatchTouchEvent方法;如果 intercepted 为 true,则会强制给 mFirstTouchTarget 对应的 child 分发一个 ACTION_CANCEL 事件,然后将 mFirstTouchTarget 置为 null
E. ViewGroup 的 dispatchTouchEvent 事件分发流程
我们将一个完整的事件序列的整体流程走一遍
- 当 ACTION_DOWN 事件时,会将 mFirstTouchTarget 置为 null
- 然后判断 onInterceptTouchEvent 是否拦截,如果返回 true,则后续事件都不会再调用 onInterceptTouchEvent 方法来判断是否拦截并且默认都是 true
- 因为 intercepted 为 true,并且 mFirstTouchTarget 为 null,因此会直接调用自身的 super.dispatchTouchEvent 方法。并且后续事件都会如此执行(若下一个事件能到达)。
- 如果第2步的 onInterceptTouchEvent 返回 false,则会尝试找到一个满足条件的子 View,分发此次 ACTION_DOWN 事件,并对 mFirstTouchTarget 进行赋值
- 如果第4步找不到合适的子 View,mFirstTouchTarget 依然为 null,则会调用自身的 super.dispatchTouchEvent 方法。并且后续事件都会如此执行(若下一个事件能到达)。
- 如果第4步找到了合适的子 View,也就意味着分发了一个 ACTION_DOWN 事件,那么 ACTION_DOWN 就什么都不用做了。
- 当 ACTION_MOVE 和 ACTION_UP 事件时,如果第4步找到了合适的子 View,mFirstTouchTarget 被赋值,那么会再次判断 onInterceptTouchEvent 是否拦截。
- 如果第7步判断为不拦截,则直接分发此次事件给 mFirstTouchTarget 对应的 child
- 如果第7步判断为拦截,则强制分发 ACTION_CANCEL 事件给 mFirstTouchTarget 对应的 child,然后将 mFirstTouchTarget 置为 null
3. View事件分发
public boolean dispatchTouchEvent(MotionEvent event) {
......
if (onFilterTouchEventForSecurity(event)) {
......
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
看完了 ViewGroup 的 dispatchTouchEvent,再来看 View 的 dispatchTouchEvent 就发现是不是太简单了。这里简单提醒一下:在 ViewGroup 调用 super.dispatchTouchEvent 其实就是调用的 View 的 dispatchTouchEvent 方法。
首先,如果设置了 mOnTouchListener,则会优先处理 mOnTouchListener 的 onTouch 方法,如果 onTouch 方法返回 true,则此方法直接返回而不会再调用 onTouchEvent 方法了。也就是说 mOnTouchListener 的优先级高于 onTouchEvent 方法。如果在 mOnTouchListener 的 onTouch 方法中返回了 true,也许会因为没有执行 onTouchEvent 方法而导致点击等回调不被调用。最后会将 mOnTouchListener 或 onTouchEvent 方法的返回值作为此方法的返回值。
然后我们看看 onTouchEvent 方法
public boolean onTouchEvent(MotionEvent event) {
......
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
......
return clickable;
}
......
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
......
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
......
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
......
break;
......
}
return true;
}
return false;
}
首先,只要可以点击或者长按,clickable 就为 true。不管当前控件是不是 DISABLED,都返回 clickable。clickable 根据不同的控件而值不同,比如 Button 就为 true,TextView 就为 false。设置对应的点击监听器会默认将 clickable 设为 true。也就是说,只要当前控件是可点击的,那么 onTouchEvent 默认返回 true 而消耗这一串事件序列。
在 ACTION_UP 中,会完成点击事件的判断,具体是通过 performClickInternal 方法完成的
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
也就是通过 performClick 方法来完成的
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
在这里,显示触发 mOnClickListener 的 onClick 方法来完成点击事件的回调。
滑动冲突解决
根据以上源代码分析,我们可以找到一种解决滑动冲突的套路:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
// 横向滑动则自己处理,竖向滑动则子View处理
if (Math.abs(x - lastX) > Math.abs(y - lastY)){
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
lastX = x;
lastY = y;
return intercept;
}
- ACTION_DOWN 事件一定要返回 false,否则所有的事件都会被 ViewGroup 拦截,并且不会再调用 onInterceptTouchEvent 方法。因此 ACTION_DOWN 事件一定会被子 View 所消耗。如果当前 ViewGroup 需要在 ACTION_DOWN 处理一些逻辑,则可以在 dispatchTouchEvent 或者 onInterceptTouchEvent 方法处理
- ACTION_MOVE 事件根据需要来决定是否拦截,一旦拦截,则会立马给 mFirstTouchTarget 指定的 child 分发一个 ACTION_CANCEL 事件,后续的事件序列都直接交由 ViewGroup 消耗,并且不会再调用 onInterceptTouchEvent 方法
- ACTION_UP 事件的返回值返回 false,如果在之前的事件序列返回过 true,那么 ACTION_UP 返回什么都无所谓,因为根本不会被执行到;但是如果之前的事件序列没有返回过 true,那么 ACTION_UP 返回 true,会直接强制替换为 ACTION_CANCEL 分发给子 View,导致子 View 的点击事件无法响应。