文章

应用层事件分发

应用层事件分发

事件处理(View)

基于 Android29,View 的事件处理入口 dispatchTouchEvent

boolean dispatchTouchEvent(MotionEvent event)

传递 ACTION_DOWN 事件给 View 或该 View 是 target 了;true 表示消费事件,false 为消费

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
// View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
    // If the event should be handled by accessibility focus first.
    if (event.isTargetAccessibilityFocus()) {
        // We don't have focus or no virtual descendant has it, do not handle the event.
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        // We have focus and got the event, then use normal event dispatch.
        event.setTargetAccessibilityFocus(false);
    }

    boolean result = false;

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        stopNestedScroll();
    }

    if (onFilterTouchEventForSecurity(event)) { // 校验window或事件是否OBSCURED
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { // View是否enabled
            result = true;
        }
        
        // View可用,设置了OnTouchListener
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) { // onTouch返回了返回,就消费了事件
            result = true;
        }

        if (!result && onTouchEvent(event)) { // result返回true,就不会调用onTouchEvent了
            result = true;
        }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}
  1. 事件先交给 accessibility event.isTargetAccessibilityFocus() 处理
  2. View 要 Enable

onTouchEvent(MotionEvent event)   处理单击,长按

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
// View#onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

    if ((viewFlags & ENABLED_MASK) == DISABLED) { // View disabled
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return clickable; // 如果一个View设置为disable了,但设置了click和longclick监听,依然会consume消费事件,只是不会响onClick或onLongClick监听而已
    }
    
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                if ((viewFlags & TOOLTIP) == TOOLTIP) {
                    handleTooltipUp();
                }
                if (!clickable) { // 不可点击,移除tap、长按监听
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                }
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // take focus if we don't have it already and we should in
                    // touch mode.
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        // The button is being released before we actually
                        // showed it as pressed.  Make it show the pressed
                        // state now (before scheduling the click) to ensure
                        // the user sees it.
                        setPressed(true, x, y);
                    }

                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // This is a tap, so remove the longpress check
                        removeLongPressCallback();

                        // 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)) {
                                performClickInternal(); // 点击声音,回调
                            }
                        }
                    }

                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }

                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }

                    removeTapCallback();
                }
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_DOWN:
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                mHasPerformedLongPress = false;

                if (!clickable) {
                    checkForLongClick(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    break;
                }

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    checkForLongClick(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); // 检测longPress事件,在500ms内没有click事件,那么执行onLongClick
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                if (clickable) {
                    setPressed(false);
                }
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                break;

            case MotionEvent.ACTION_MOVE:
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }

                final int motionClassification = event.getClassification();
                final boolean ambiguousGesture =
                        motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
                int touchSlop = mTouchSlop;
                if (ambiguousGesture && hasPendingLongPressCallback()) {
                    final float ambiguousMultiplier =
                            ViewConfiguration.getAmbiguousGestureMultiplier();
                    if (!pointInView(x, y, touchSlop)) {
                        // The default action here is to cancel long press. But instead, we
                        // just extend the timeout here, in case the classification
                        // stays ambiguous.
                        removeLongPressCallback();
                        long delay = (long) (ViewConfiguration.getLongPressTimeout()
                                * ambiguousMultiplier);
                        // Subtract the time already spent
                        delay -= event.getEventTime() - event.getDownTime();
                        checkForLongClick(
                                delay,
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    touchSlop *= ambiguousMultiplier;
                }

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, touchSlop)) {
                    // Outside button
                    // Remove any future long press/tap checks
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }

                final boolean deepPress =
                        motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
                if (deepPress && hasPendingLongPressCallback()) {
                    // process the long click action immediately
                    removeLongPressCallback(); // MOVE事件移除longPress的callback
                    checkForLongClick(
                            0 /* send immediately */,
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
                }

                break;
        }

        return true;
    }

    return false;
}

View 事件处理总结

OnTouchListener 和 OnClickListener

mOnTouchListener.onTouch(this, event) 该 View 的 onTouch() 返回值
返回 true,设置了 OnTouchListener 监听,onTouch() 返回 true,View 的 onTouchEvent() 不会执行
返回 false,没有设置 OnTouchListener 监听或者 onTouch() 返回 false,执行 onTouchEvent() 方法
注: 如果要屏蔽 onTouchEvent() 事件,直接在 onTouch() 中返回 true 就行了

View disable 是否可消费事件?

一个 View 设置为了 disable,但是其可 clickable 或 longclickable,那么也会消费该事件,只是不会调用 onClick() 和 onLongClick() 方法。

单击事件和长按事件区别

  1. 单击:按下 (down),然后在 500ms 内抬起来 (up)
  2. 长按:按下 (down,超过 500ms),不需要 up 起来
  3. 单击事件,按下和抬起时间间隔小于 500ms,属于单击事件;按下超过 500ms,那么会响应长按事件,再响应单击事件

View 的长按事件,都是在 DOWN 事件中,通过 postDelayed(),默认延迟 500 毫秒,如果超过 500ms 没有 UP 事件或这期间没有 MOVE 事件到来,那么认为是长按事件,调用 onLongClick() 方法;超过了 500ms,认为是单击事件,调用 onClick() 方法。

事件分发(ViewGroup)

分发流程

1
2
3
4
5
6
7
Activity#dispatchTouchEvent 

PhoneWindow#superDispatchTouchEvent  

DecorView(FrameLayout)#superDispatchTouchEvent  

ViewGroup#dispatchTouchEvent

小结

  1. 每次 ACTION_DOWN 事件到来,代表了一套新的事件,会清除之前的 touch target
  2. 事件拦截只在 ACTION_DOWN 才能拦截
  3. 只 childvisible 或 animation!=null,才有机会决定自己是否处理事件
  4. 如果 Child DOWN 事件没有拦截,但是在 MOVE 或者 UP 的时候进行了拦截;那么在第一次会将事件设置为 CANCEL 事件,并清空 mMotionTarget,直接返回 true;

事件分发的入口 ViewGroup#dispatchTouchEvent,从这里开始分析

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// ViewGroup#dispatchTouchEvent 基于Android29
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // ...

    boolean handled = false; // 
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // ACTION_DOWN事件开始,清除所有的touch target(mFirstTouchTarget)
        // Handle an initial 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; // 是否拦截,true表示拦截,false表示不拦截
        // 首次ACTION_DOWN事件
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) { 
            // 看看子View有没有通过requestDisallowInterceptTouchEvent请求父容易不要拦截事件,true表示子View期望父容易不要拦截事件,false表示父容器能拦截事件
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) { 
                // disallowIntercept=false,看父容器onInterceptTouchEvent返回,true拦截,false不拦截
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // 不是ACTION_DOWN事件或mFirstTouchTarget不为null,父容器拦截事件
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }

        // If intercepted, start normal event dispatch. Also if there is already
        // a view that is handling the gesture, do normal event dispatch.
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        // Check for cancelation.
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // Update list of touch targets for pointer down, if needed.
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {

            // If the event is targeting accessibility focus we give it to the
            // view that has accessibility focus and if it does not handle it
            // we clear the flag and dispatch the event to all children as usual.
            // We are looking up the accessibility focused host to avoid keeping
            // state since these events are very rare.
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);

                        // If there is a view that has accessibility focus we want it
                        // to get the event first and if not handled we will perform a
                        // normal dispatch. We may do a double iteration but this is
                        // safer given the timeframe.
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }

                        if (!child.canReceivePointerEvents()
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // Child is already receiving touch within its bounds.
                            // Give it the new pointer in addition to the ones it is handling.
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        resetCancelNextUpFlag(child);
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                // childIndex points into presorted list, find original index
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        // The accessibility focus didn't handle the event, so clear
                        // the flag and do a normal dispatch to all children.
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }

                // 只有down事件进来,这个处理意味着多指事件下只有一个child能响应down事件,那不管你其他指头落在哪里,都默认是这个view处理(已经有一个view消费了,再触摸到其他的未消费事件的view上执行)
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // 首次ACTION_DOWN事件到来,mFirstTouchTarget为null
        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            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;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // Update list of touch targets for pointer up or cancel, if needed.
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

一套事件有一个 ACTION_DOWN 事件,0 或多个 ACTION_MOVE 事件,1 个 ACTION_UP 事件

onInterceptTouchEvent 总结

你说说 viewgroup 的 onInterceptTouchEvent 执行情况,有子 view 消费和无子 view 消费的情况

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
public boolean dispatchTouchEvent(MotionEvent ev) {
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) { // Down事件或者mFirstTouchTarget不为空(有子view消费事件)
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) { // 子类未设置FLAG_DISALLOW_INTERCEPT标记,父容器是否拦截事件取决onInterceptTouchEvent
            intercepted = onInterceptTouchEvent(ev); // onInterceptTouchEvent返回false不拦截,返回true拦截
            ev.setAction(action); // restore action in case it was changed
        } else { // 子类设置了FLAG_DISALLOW_INTERCEPT标记通知父容器不拦截事件,那么父容器不拦截事件
            intercepted = false;
        }
    } else { // 不是Down事件且mFirstTouchTarget为null,默认拦截(即未找到子view消费事件,那么MOVE/UP事件父容器默认拦截)
        intercepted = true;
    }
    TouchTarget newTouchTarget = null;
    if (!canceled && !intercepted) { // 事件未取消且事件未被拦截
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            final View[] children = mChildren;
            for (int i = childrenCount - 1; i >= 0; i--) {
                final boolean handled;
                handled = child.dispatchTouchEvent(event); // 倒序遍历子view分发事件
                if(handled) {
                    newTouchTarget = addTouchTarget(child, idBitsToAssign); // mFirstTouchTarget在这赋值,找到可消费事件的子View
                    break;
                }
            }
        }
    }
    if (mFirstTouchTarget == null) { // 未找到可消费事件的子view,调用view#dispatchTouchEvent,最后调用onTouchEvent自己处理事件
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else { // 找到了可消费事件的子view
        TouchTarget target = mFirstTouchTarget;
        while (target != null) { // 遍历子mFirstTouchTarget
            final TouchTarget next = target.next;
            if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { // 分发事件给mFirstTouchTarget
                handled = true;
            }
            target = next;
        }
    }
}

onInterceptTouchEvent 返回 false

  1. onInterceptTouchEvent 返回 false,即父容器不不拦截事件,此时 Down 事件会遍历子控件看是否消费事件,但子 View 未找到消费的事件;这个时候会调用父控件自身的 onTouchEvent 处理事件(down 事件父控件的 onInterceptTouchEvent 和子控件都会走;后续的 move/up 事件,父控件的 onInterceptTouchEvent 和所有子 view 都不会接收该事件了)
  2. onInterceptTouchEvent 返回 false,即父容器不拦截事件,此时 Down 事件会遍历子控件看是否消费事件,找到了一个子 View 能消费事件;这个时候 mFirstTouchTarget 就不为 null 有值了,就会将后续 move/up 交给 mFirstTouchTarget 消费(down 事件父控件的 onInterceptTouchEvent 和子控件都会走;后续的 move/up 事件,父控件的 onInterceptTouchEvent 和消费事件的子控件会走)

onInterceptTouchEvent 返回 true

  1. onInterceptTouchEvent 返回 true,即父容器拦截该事件,此时 Down 事件不会经过子控件(也就是子控件收不到任何 down/move/up 事件),会调用父控件自身的 onTouchEvent 事件处理(down 事件父控件的 onInterceptTouchEvent 会走,子控件不走;后续 move/up 事件父控件的 onInterceptTouchEvent 不会走)

ACTION_DOWN 事件

  1. 首先看是否拦截事件,intercepted 为 true 表拦截,false 不拦截
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
  1. 如果 intercepted=true,拦截了事件,ViewGroup 自己处理事件,走 dispatchTransformedTouchEvent()
1
2
3
4
5
6
7
8
if (mFirstTouchTarget == null) {
    // 没有touch targets所以当成普通的view处理
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS); // canceled此时为false,child为null
} else {
    /// ...
}

接着走 dispatchTransformedTouchEvent()

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
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    // Calculate the number of pointers to deliver.
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    // If for some reason we ended up in an inconsistent state where it looks like we
    // might produce a motion event with no pointers in it, then drop the event.
    if (newPointerIdBits == 0) {
        return false;
    }

    // If the number of pointers is the same and we don't need to perform any fancy
    // irreversible transformations, then we can reuse the motion event for this
    // dispatch as long as we are careful to revert any changes we make.
    // Otherwise we need to make a copy.
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                // child为null,调用View#dispatchTouchEvent
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}

调用 View#dispatchTouchEvent 处理事件

  1. 如果 intercepted=false,不拦截事件,寻找 touch target(mFirstTouchTarget)
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
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (actionMasked == MotionEvent.ACTION_DOWN) { // 为ACTION_DOWN事件
    final int childrenCount = mChildrenCount;
    if (newTouchTarget == null && childrenCount != 0) { // newTouchTarget为null且childrenCount不为0(有孩子)
        final View[] children = mChildren;
        for (int i = childrenCount - 1; i >= 0; i--) { // 拿最后一个孩子
            if (!child.canReceivePointerEvents() && !isTransformedTouchPointInView(x, y, child, null)) { // 判断child是否VISIBLE||mCurrentAnimation!=null 叽叽判断触摸点是否在childView上,不在的话continue,不分发事件
                continue;
            }
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 调用子View的dispatchTouchEvent处理事件
                // Child wants to receive touch within its bounds.
                newTouchTarget = addTouchTarget(child, idBitsToAssign); // 首次ACTION_DOWN事件,target.next=null,mFirstTouchTarget = 当前子View的target
                alreadyDispatchedToNewTouchTarget = true; // 为true
                break; // 找到一个可以消费事件的子View,结束事件分发了
            }
        }
        if (newTouchTarget == null && mFirstTouchTarget != null) { // 首次ACTION_DOWN事件,找到了newTouchTarget不为null,不走这段逻辑
            // Did not find a child to receive the event.
            // Assign the pointer to the least recently added target.
            newTouchTarget = mFirstTouchTarget;
            while (newTouchTarget.next != null) {
                newTouchTarget = newTouchTarget.next;
            }
            newTouchTarget.pointerIdBits |= idBitsToAssign;
        }
    }
}

// View#pointInView,判断触摸点是否在View上
public boolean pointInView(float localX, float localY, float slop) {
    return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
            localY < ((mBottom - mTop) + slop);
}

// 获取一个TouchTarget
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

看看找到了一个 touch target(看 mFirstTouchTarget!=null 了)

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
if (mFirstTouchTarget == null) { // 不拦截事件,未找到一个子View,
   // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else { // 不拦截事件,找到了一个子View(touch target),现在走这里 ,首次ACTION_DOWN事件,走到这里,已经事件被消费了,在这里什么也没做
    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it.  Cancel touch targets if necessary.
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next; // 首次mFirstTouchTarget=null
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { // 首次ACTION_DOWN事件,如果找到了touch target,那么alreadyDispatchedToNewTouchTarget=true
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}

到这里,ACTION_DOWN 事件的拦截和非拦截都分析完毕

  1. 拦截,调用 View#dispatchTouchEvent 处理事件 (此处 ViewGroup 自己处理事件)
  2. 不拦截,未找到一个 child 能消费事件(mFirstTouchTarget=null),调用 View#dispatchTouchEvent 处理事件 (此处 ViewGroup 自己处理事件)
  3. 不拦截,找到一个 child 能消费事件(mFirstTouchTarget!=null),child#dispatchTouchEvent 处理事件(此处由指定的 child 处理事件)

ACTION_MOVE 事件

首次 MOVE_ 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) { // 现在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;
}

看看父容器是否拦截事件

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
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
    // MOVE事件这里什么也没做
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) { // MOVE事件,之前未找到child touch target,那么还是调用父容器View.dispatchTouchEvent自己处理事件
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else { // MOVE事件,之前有找到child touch target
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { // MOVE事件alreadyDispatchedToNewTouchTarget=false
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;  // MOVE事件,如果DOWN事件找到了要分发的子View,现在父容器要拦截掉该事件,那么需要取消分发Cancel事件给子View
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {  // 之前将事件分发给自己找到的child touch target
                handled = true;
            }
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next; // mFirstTouchTarget只为null了
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}
// 此时为MOVE事件,acncel为true,child不为null
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { // 如果事件被取消了,分发cancel给子view,这次MOVE事件到此为止
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    // ...
}

到这里,首次 ACTION_MOVE 事件的拦截和非拦截都分析完毕

  1. 拦截,之前未找到一个 child 能消费事件,调用 View#dispatchTouchEvent 处理事件 (此处 ViewGroup 自己处理事件)
  2. 拦截,之前找到一个 child 能消费事件,给 child 分发个 cancel 事件
  3. 不拦截,之前未找到一个 child 能消费事件(mFirstTouchTarget=null),调用 View#dispatchTouchEvent 处理事件 (此处 ViewGroup 自己处理事件)
  4. 不拦截,之前找到一个 child 能消费事件(mFirstTouchTarget!=null),child#dispatchTouchEvent 处理事件(此处由指定的 child 处理事件)

第 2+ 次 MOVE 时间

无 touch target

1
2
3
4
5
6
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

无 touch target,直接调用父 View 自己的 View#dispatchTouchEvent 处理时间

有 touch target,拦截

有 touch target,在 MOVE 时被拦截了,那么 mFirstTouchTarge 为 null,同无 touch target 了

有 touch target,不拦截

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
TouchTarget predecessor = null;
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;
        if (dispatchTransformedTouchEvent(ev, cancelChild,
                target.child, target.pointerIdBits)) {
            handled = true;
        }
        if (cancelChild) {
            if (predecessor == null) {
                mFirstTouchTarget = next;
            } else {
                predecessor.next = next;
            }
            target.recycle();
            target = next;
            continue;
        }
    }
    predecessor = target;
    target = next;
}

有 touch target,不拦截,子 View 调用自己的 dispatchTouchEvent

ACTION_UP 事件

情况状态,清除 mFirstTouchTarget

1
2
3
4
5
6
7
8
9
10
11
if (canceled
        || actionMasked == MotionEvent.ACTION_UP
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    resetTouchState();
}
private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

事件冲突解决

内部拦截法解决 requestDisallowInterceptTouchEvent

  1. 如果父容器在 down 事件就给 intecept 了,那么子 view 收不到任何事件,子 view 的 dispatchTouchEvent 也不会调用,也就用不了内部解决冲突问题了
  2. 如果父容器 down 事件不 intecept,子 view 可以收到 down 事件并消费,但在 move 事件时给拦截了,可以内部调用 View#requestDisallowIntecept(disallow) 解决事件冲突, 设置 true 递归父容器不拦截,false 拦截
  3. 要在 down 调用 requestDisallowInterceptTouchEvent(true),事件冲突一般都是在 move 来解决;在 up 时恢复 requestDisallowInterceptTouchEvent(false)
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
public class MyListView extends ListView {
    public MyListView(Context context) {
        super(context);
    }
    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    float startX = 0F;
    float startY = 0F;
    // 内部拦截法:子view处理事件冲突
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                startX = ev.getX();
                startY = ev.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                float deltX = ev.getX() - startX;
                float deltY = ev.getY() - startY;
                if (Math.abs(deltX) > Math.abs(deltY)) {
                    requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                requestDisallowInterceptTouchEvent(false);
                break;
            default:
                break;
        }
        boolean b = super.dispatchTouchEvent(ev);
        return b;
    }
}

外部拦截法解决 onInterceptTouchEvent

外部拦截法:父容器处理冲突
父容器 onInterceptTouchEvent,我想要把事件分发给谁就分发给谁

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
public class BadViewPager extends ViewPager {
    private int mLastX, mLastY;
    public BadViewPager(@NonNull Context context) {
        super(context);
    }
    public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    // 外部拦截法:父容器处理冲突
    // 我想要把事件分发给谁就分发给谁
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
//        if (event.getAction() == MotionEvent.ACTION_DOWN){
//            super.onInterceptTouchEvent(event);
//            return false;
//        }
//        return true;

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                mLastX = (int) event.getX();
                mLastY = (int) event.getY();
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    return true;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }
        return super.onInterceptTouchEvent(event);
    }
}
本文由作者按照 CC BY 4.0 进行授权