文章

Android View 事件分发机制与源码解析

Android View 事件分发机制与源码解析

View 的事件分发机制是 Android UI 交互的核心,也是面试中必问的重中之重。很多滑动冲突(如 ScrollView 嵌套 RecyclerView)的本质,都需要利用事件分发机制来解决。

本文将配合源码,把 Android 的事件分发机制彻底梳理清楚。

一、 核心概念与三个重要方法

在 Android 中,一次完整的触摸交互通常由一系列的 MotionEvent 组成:

  • ACTION_DOWN:手指初次接触屏幕(最重要,是一次事件流的开始)。
  • ACTION_MOVE:手指在屏幕上滑动。
  • ACTION_UP:手指离开屏幕。
  • ACTION_CANCEL:非人为因素导致的事件取消(通常是被父 View 拦截了原本属于子 View 的事件)。

整个分发流程主要围绕三个核心方法展开:

  1. dispatchTouchEvent(MotionEvent ev)
    • 作用:负责事件的分发。只要事件传递到了当前 View,该方法就一定会被调用。
    • 返回值:返回 true 表示事件被当前 View 或其子 View 消费;返回 false 表示不消费。
  2. onInterceptTouchEvent(MotionEvent ev)
    • 作用:负责事件的拦截。只有 ViewGroup 才有此方法,普通 View 没有。
    • 返回值:返回 true 表示拦截该事件,不继续下发给子 View,而是交由自己的 onTouchEvent 处理;返回 false 表示不拦截,继续分发。
  3. onTouchEvent(MotionEvent ev)
    • 作用:负责事件的处理。
    • 返回值:返回 true 表示消费了该事件(事件到此为止);返回 false 表示不消费,事件将回传给父 View 的 onTouchEvent。

二、 整体分发流程 (U 型图)

当手指触摸屏幕时,事件的传递顺序是: Activity -> Window -> DecorView -> ViewGroup (父 View) -> … -> View (子 View)

这是一个“从外向内”的分发(下沉)过程。如果最底层的子 View 也没有消费(onTouchEvent 返回 false),事件就会“从内向外”回传(上浮),逐层调用父 View 的 onTouchEvent,直到抛给 Activity。

如果用伪代码来表示 ViewGroup 的分发核心逻辑,非常经典:

1
2
3
4
5
6
7
8
9
10
11
12
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    // 1. 判断是否拦截
    if (onInterceptTouchEvent(ev)) {
        // 2. 如果拦截,则交由自己的 onTouchEvent 处理
        consume = onTouchEvent(ev);
    } else {
        // 3. 如果不拦截,下发给子 View
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

三、 源码深度解析

1. Activity 的下发起点

事件最先到达 Activity 的 dispatchTouchEvent:

1
2
3
4
5
6
7
8
9
10
11
12
// Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // 将事件交给 Window 处理
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    // 如果所有 View 都不消费,最后交给 Activity 自己处理
    return onTouchEvent(ev);
}

Window 的唯一实现类是 PhoneWindow,它会直接把事件交给根 View DecorView(它是一个 FrameLayout/ViewGroup),从而正式进入 View 树的分发。

2. ViewGroup 的分发核心:dispatchTouchEvent

这里是重点,我们剥离出最核心的逻辑(基于 Android API 30+ 简化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    final int action = ev.getActionMasked();
    
    // 1. 清理状态:如果是 DOWN 事件,清理之前的触摸目标(mFirstTouchTarget)
    if (action == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }

    // 2. 决定是否拦截!
    final boolean intercepted;
    // 只有 DOWN 事件,或者已经有子 View 处理了事件(mFirstTouchTarget != null),才允许拦截
    if (action == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        // 子 View 有没有要求父 View 不要拦截?(FLAG_DISALLOW_INTERCEPT)
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev); // 调用父类的 onInterceptTouchEvent
            ev.setAction(action); // 恢复 action 防止被篡改
        } else {
            intercepted = false; // 子 View 说不能拦,就不拦
        }
    } else {
        // 不是 DOWN 事件,且没有子 View 消费事件,那后续事件父 View 直接自己拦截
        intercepted = true;
    }

    // 3. 如果没有被拦截,开始找子 View 分发
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    
    if (!canceled && !intercepted) {
        if (action == MotionEvent.ACTION_DOWN || /* 某些其他情况 */) {
            final int childrenCount = mChildrenCount;
            if (childrenCount != 0) {
                // 倒序遍历子 View(通常最后添加的在最上层,比如 Z 轴最高的 View 优先响应)
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final View child = getAndVerifyPreorderedView(...);
                    
                    // 判断触摸点是否在这个 child 内部,或者 child 是否在播放动画
                    if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
                        continue;
                    }

                    // 【核心下发】将事件分发给子 View!
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        // 子 View 消费了事件!
                        // 记录处理事件的子 View 到 mFirstTouchTarget 链表中
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                        break; // 找到了处理的子 View,结束循环!
                    }
                }
            }
        }
    }

    // 4. 如果没有子 View 消费,或者被拦截了
    if (mFirstTouchTarget == null) {
        // 把自己当做普通的 View 来处理(注意第三个参数 child 传了 null)
        // 内部会调用 super.dispatchTouchEvent(ev),即最终走到自己的 onTouchEvent
        handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
    } else {
        // 5. 将后续事件(MOVE/UP)分发给刚才记下来的消费了 DOWN 事件的子 View
        // 如果后面父 View 中途拦截了,会给子 View 发送一个 ACTION_CANCEL
        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;
                // 如果父 View 决定中途拦截(intercepted=true),传给子 View 的就是 CANCEL 事件
                if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                    handled = true;
                }
                if (cancelChild) {
                    if (predecessor == null) { mFirstTouchTarget = next; }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            target = next;
        }
    }
    return handled;
}

3. 普通 View 的事件处理:dispatchTouchEvent

如果事件交给了某个普通 View(或者 ViewGroup 选择自己处理),就会进入 View 的 dispatchTouchEvent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    
    if (onFilterTouchEventForSecurity(event)) {
        // 1. 优先判断 OnTouchListener
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null 
                && (mViewFlags & ENABLED_MASK) == ENABLED // 前提是 View 是 enabled 的
                && li.mOnTouchListener.onTouch(this, event)) {
            // 如果 onTouch 返回了 true,result 置为 true
            result = true; 
        }

        // 2. 如果 onTouch 没消费(返回 false),才去调用 onTouchEvent
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    return result;
}

面试必考:onTouch 和 onTouchEvent 谁优先级高? 源码清清楚楚:OnTouchListener.onTouch 优先级高于 onTouchEvent。如果 onTouch 返回 true,onTouchEvent 将不会被调用。而我们平时经常用的 setOnClickListener(点击事件)其实是在 onTouchEvent 内部触发的。

4. 点击事件的源头:onTouchEvent

来看看 View.onTouchEvent 是怎么处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// View.java
public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    // 1. 如果 View 是被禁用的(disabled),只要它是 clickable 的,照样会消耗事件!只是不响应。
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE));
    }

    // 2. 判断是否是 clickable(点击、长按等)
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                // ... 处理按下状态
                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    removeLongPressCallback();
                    // 触发点击事件!
                    performClickInternal(); 
                }
                break;
            case MotionEvent.ACTION_DOWN:
                // 设置按下状态,并发送延迟消息用于检测长按事件 (LongClick)
                checkForLongClick(0, x, y);
                break;
            case MotionEvent.ACTION_CANCEL:
                // 清理状态
                removeTapCallback();
                removeLongPressCallback();
                setPressed(false);
                break;
            case MotionEvent.ACTION_MOVE:
                // 如果滑动超出了 View 边界,取消长按和点击状态
                if (!pointInView(x, y, mTouchSlop)) {
                    removeTapCallback();
                    removeLongPressCallback();
                    // ...
                }
                break;
        }
        return true; // 只要可点击,默认就会消费掉事件
    }
    return false;
}

private boolean performClickInternal() {
    // 最终调用 onClickListener.onClick()
    return performClick(); 
}

可见,OnClickListener 的 onClick 方法是在 ACTION_UP 的时候触发的。而长按事件 onLongClick 是在 ACTION_DOWN 时通过 Handler 发送延时任务触发的。如果手指移动出了控件边界,就会取消这个延时任务。


四、 经典面试连环问

1. ACTION_DOWN 为什么如此重要?

因为 ACTION_DOWN 是一系列事件的开端。ViewGroup 只有在收到 ACTION_DOWN 时,才会清空之前的状态,重新寻找目标子 View,并给 mFirstTouchTarget 赋值。 如果一个 View 的 onTouchEvent 在处理 ACTION_DOWN 时返回了 false,那么后续的 ACTION_MOVE 和 ACTION_UP 将永远不会传给它(因为父 View 认为它不想处理这次事件流,不会将其加入 mFirstTouchTarget 链表)。

2. 子 View 如何强制不让父 View 拦截事件?(解决滑动冲突的核心)

子 View 可以调用 getParent().requestDisallowInterceptTouchEvent(true)。 在 ViewGroup 的分发源码中,一旦标志位 FLAG_DISALLOW_INTERCEPT 被设置,父 View 就会跳过 onInterceptTouchEvent 的执行。 注意:这招对 ACTION_DOWN 无效。因为父 View 在处理 DOWN 事件前,一定会调用 resetTouchState() 清除掉这个标志位。DOWN 事件必定会经过父 View 的 onInterceptTouchEvent 询问。

3. ViewGroup 什么时候会下发 ACTION_CANCEL 事件给子 View?

如果父 View 之前把 ACTION_DOWN 等事件交给了子 View 处理,但是在后续的 ACTION_MOVE 中,父 View 突然决定自己要拦截事件(例如,检测到了长距离滑动,判断为在划动外部 ScrollView)。 此时,父 View 会把自身的 intercepted 设为 true。然后在遍历 mFirstTouchTarget 链表给子 View 下发时,把 cancelChild 设为 true,从而强行传给子 View 一个 ACTION_CANCEL 事件,并在下一循环将子 View 踢出 mFirstTouchTarget 链表。

4. 为什么给 View 设置 setOnClickListener 后,事件就被消费了?

因为 setOnClickListener 源码中会自动把 View 变成 CLICKABLE = true。而在 onTouchEvent 中,只要 View 是 clickable 的,无论最后有没有执行 onClick,都会直接返回 true,从而截断事件的上浮。

本文由作者按照 CC BY 4.0 进行授权