Android事件分发过程解析

什么是事件分发

当手指触摸屏幕的过程中,系统会创建一系列的MotionEvent对象,并将此对象传递给一个具体的View进行处理,这个传递过程即为分发过程,MotionEvent即为分发的事件。典型的事件分为三个类型:

  • ACTION_DOWN —— 表示手指按下屏幕
  • ACTION_MOVE —— 表示手指在屏幕上滑动
  • ACTION_UP —— 表示手指离开屏幕

一个完整的事件序列以ACTION_DOWN开始,到ACTION_UP结束。传递时涉及三个重要的方法:

  • public boolean dispatchTouchEvent(MotionEvent event):用来进行事件的分发,返回结果表示是否消耗当前事件
  • public boolean onInterceptTouchEvent(MotionEvent event):用于判断是否拦截某个事件
  • public boolean onTouchEvent(MotionEvent event)用于处理事件,返回结果表示是否消耗当前事件

通过伪代码表示事件分发过程:

1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(event);
}
return consume;
}

Activity与Window的事件分发过程

当一个事件产生时,事件最先传递给当前ActivitydispatchTouchEvent()进行分发:

1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

Activity只是简单的将事件传递给了当前ActivityWindow进行分发,当WindowsuperDispatchTouchEvent()方法返回true时,事件分发完成,否则交给当前ActivityonTouchEvent()分发处理。接着看PhoneWindow的源码:

1
2
3
4
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}

这里直接传递给了DecorViewsuperDispatchTouchEvent()方法进行分发,再接着往下看:

1
2
3
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}

DecorView将事件传递给了ViewGroupdispatchTouchEvent()方法进行分发,至此事件传递到了View

ViewGroup的事件分发

对于ViewGroup的点击事件,分发过程如下:

  1. 初始化:当事件是ACTION_DOWN时,清除之前事件序列的所有状态以及mFirstTouchTarget
  2. 判断是否拦截事件:
    • 当事件是ACTION_DOWN时,如果没有设置FLAG_DISALLOW_INTERCEPT标记位,是否拦截事件由ViewGrouponInterceptTouchEvent()方法决定,否则ViewGroup不拦截事件
    • 当事件不是ACTION_DOWN时,如果mFirstTouchTarget != null,那么与上一条类似,是否拦截事件由FLAG_DISALLOW_INTERCEPT标记位与onInterceptTouchEvent()方法决定。否则ViewGroup将拦截事件
  3. ViewGroup不拦截事件且事件为ACTION_DOWN时,调用removePointersFromTouchTargets()移除之前事件序列的TouchTarget,并跳过遍历寻找能够处理该事件的View,寻找过程如下:
    • 从后往前遍历,使上层的View能优先收到事件
    • 寻找能够接收事件的View,判断条件为View可见或处于动画中,并且事件坐标在View上
    • 如果该View已经在mFirstTouchTarget的单链表上,那么结束寻找
    • 如果该View不在mFirstTouchTarget的单链表上,通过dispatchTransformedTouchEvent()对调用child的dispatchTouchEvent()方法对事件进行分发,如果child消费此事件,则将此View添加到mFirstTouchTarget的单链表上并设置alreadyDispatchedToNewTouchTarget为true,结束寻找
    • 重复,直到遍历完所有child或者找到符合条件的view
    • 如果没有找到满足条件的View且mFirstTouchTarget != null,将最新添加的TouchTarget作为找到的view
  4. 判断mFirstTouchTarget是否为空,即是否有child消费了此事件序列的ACTION_DOWN事件,如果没有则自己处理该事件。
  5. 如果mFirstTouchTarget不为空,即有child消费了此事件序列的ACTION_DOWN事件,则将此事件交给该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
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
216
217
218
@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. 初始化
// 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();
}

//2. 判断是否拦截事件
// 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;
}

// 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;

//3. 当ViewGroup不拦截事件且事件为ACTION_DOWN时
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();
//遍历children寻找接收此事件的View
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;
}

//如果View不可见并且不是处于动画状态,或者事件坐标不在View上,
//这样的View显然不满足条件,直接continue
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}

//如果view在mFirstTouchTarget的单链表上,结束查找
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;
}

//如果view不在mFirstTouchTarget的单链表上
//调用dispatchTransformedTouchEvent()方法将事件传递给子view
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();
}

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;
}
}
}

//4. 判断是否有child消费了ACTION_DOWN事件
// 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;
}

从上面的代码中可以发现,在对事件进行分发时,都是调用的:

  • private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)

看一下此方法的源码,虽然源码较长,但是可以发现,当child == null时将调用super.dispatchTouchEvent()即View类的dispatchTouchEvent()方法,否则将调用child.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
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) {
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(不包含ViewGroup)的事件分发

由于View不再包含child所以不会进一步向下传递事件,当View处于可用状态且设置了OnTouchListener时,优先将事件传递给OnTouchListener处理。当不满足条件或OnTouchListener不消费当前事件(返回了false)时,事件将传递给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
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
... ...

if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
}
... ...

return result;
}

对于onTouchEvent()方法,主要分为几个部分:

  1. 当View处于不可用状态时,如果当前View可点击(CLICKABLELONG_CLICKABLE,下同)则会消耗当前事件,但是不会响应事件。
  2. 当View设置有TouchDelegate(用于扩大View的可点击范围),将事件传递给TouchDelegateonTouchEvent()方法处理
  3. 当View处于可点击状态时,所有事件都将被消费掉,并且在ACTION_DOWN时,发送延迟消息(默认500ms)来执行长按事件。在ACTION_UP时,判断长按事件是否已执行,如果没有执行则移除长按事件的回调,并调用performClick()方法执行点击事件。
    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
    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;

    //1. 当View不可用时的处理过程
    if ((viewFlags & ENABLED_MASK) == 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;
    }
    //2. 当View设置有代理时的处理过程
    if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
    return true;
    }
    }
    //3. 对View事件的具体处理
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
    case MotionEvent.ACTION_UP:
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    if ((viewFlags & TOOLTIP) == TOOLTIP) {
    handleTooltipUp();
    }
    if (!clickable) {
    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)) {
    performClick();
    }
    }
    }

    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(0, x, y);
    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(0, x, y);
    }
    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);
    }

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

    return true;
    }

    return false;
    }