View 的事件分发机制

View 的事件分发流程

Gcssloop大神

吴小龙

最关键的 boolean —

  • true 表示消费事件
  • false 表示没有能力消费事件

Activity、Window、View 三者的关系

View 的结构图如下:

View 的结构图

从上图中,我们可以知道它们三者的关系是:Activity > Window > View。

PhoneWindow 是 Window 的实现类,DecorView 是 PhoneWindow 的内部类,

事件传递的过程

事件收集之后最先传递给 Activity, 然后依次向下传递,大致如下:

1
Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View

这样的事件分发机制逻辑非常清晰,如果没有任何View消费掉事件,那么这个事件会按照反方向回传,最终传回给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃:

1
Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View

三个关键函数

  1. 分别为:
  • dispatchTouchEvent(event)
  • onInterceptTouchEvent(event)
  • onTouchEvent(event)

它们三者的关系,ViewGroup 的事件分发机制伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默认状态为没有消费过
if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View
result = child.dispatchTouchEvent(ev);
}
if (!result) { // 如果事件没有被消费,询问自身onTouchEvent
result = onTouchEvent(ev);
}
return result;
}

相应的View 的 dispatchTouchEvent(event) 代码如下:

1
2
3
4
5
6
7
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
  1. 事件分发与消费

表示有该方法。

X 表示没有该方法。

类型 相关方法 Activity ViewGroup View
事件分发 dispatchTouchEvent
事件拦截 onInterceptTouchEvent X X
事件消费 onTouchEvent

事件处理的三种情况

  • 子View 不处理事件

    1
    view.onTouchEvent(event) --- false

    在这种情况下,子 View 并不处理事件,将事件的处理逐级的向上传递直到 Activity。如果 Activity 不处理,那么事件废弃。

  • 子 View 处理事件

    1
    view.onTouchEvent(event) --- true;

    此时,子 View 将消费 Touch 事件,

  • 父 View 处理事件

    1
    view.onTouchEvnet(event) --- true

    此时,父 View 将调用onTouchEvent 方法消费 Touch 事件。

View 的事件处理

关键的代码如下:

1
2
3
4
5
6
7
8
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}

这里面设计几个概念: onTouchListener 、Enable_mask、onTouchListener.onTouch()方法。

  • onTouchListener

    它的来源如下:

    1
    2
    3
    public void setOnTouchListener(OnTouchListener l) {
    mOnTouchListener = l;
    }

    只要我们调用了 View.setOnTouchListener() 那么第一个条件就成立。

  • onTouchListener.onTouch()

    这是控件注册 onTouchListener 回调里的方法,如果返回 true,那么 dispatchTouchEvent 方法直接返回 true,否则执行 onTouchEvent(event) 方法。

  • 由此可见在onTouchListeneronTouchEvent 两者谁先执行上,我们已经又了结论:如果设置了onTouchListener,那么先执行 onTouch(event) 方法;否则执行onTouchEvent方法。

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
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
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
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:
mHasPerformedLongPress = false;
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:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}

给出两个结论:

  • onClick 方法在 onTouchEvent 里。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public boolean performClick() {
    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);
    return result;
    }
  • touch事件的层级传递

    如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。

    简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

    送上总结:

    1. Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。
    2. 在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。
    3. 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。