Android事件分发机制源码解析

话说在很多时候,特别是在自定义控件时,需要对事件传递机制要有所了解,当然,网上这方面的资料一搜一大堆,然而如果不自己整理一遍的话,还是感觉不踏实。了解Android事件分发机制并不难,难的是能一步一步搞清楚原理,结合Android源码可以帮助我们理解,所以这篇文章就诞生了。

PS: 吐槽一下,看网上很多分析的源码都很简单的,和自己看的代码感觉完全是两个风格,表示很头疼。后来发现是因为我看的是API 23的代码,新版本有很多之前没有的方法,很坑爹。写完整个文章后才知道这一点,真是醉了。Orz

现在让我们创建一个简单的Activity,创建一个TestLinearLayout继承自LinearLayout,创建一个Test继承自Button。

在TestLineaLayout类中,重写了和事件相关的代码,整个代码如下:

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 class TestLinearLayout extends LinearLayout {

public TestLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("TestLinerLayout", "onInterceptTouchEvent action = " + ev.getAction());
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("TestLinerLayout", "dispatchTouchEvent action = " + ev.getAction());
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("TestLinerLayout", "onTouchEvent action = " + event.getAction());
return super.onTouchEvent(event);
}
}

在TestButton中,同样也重写的和事件分发相关的代码,整改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestButton extends Button {

public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("TestButton", "dispatchTouchEvent action = " + event.getAction());
return super.dispatchTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("TestButton", "onTouchEvent action = " + event.getAction());
return super.onTouchEvent(event);
}
}

然后是MainActivity的XML布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<com.wl.com.testview.TestLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:id="@+id/myLayout"
android:orientation="vertical">

<com.wl.com.testview.TestButton
android:id="@+id/myButton"
android:text="Click Me"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

</com.wl.com.testview.TestLinearLayout>

最后是MainActivity的代码:

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 class MainActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {

TestButton myButton;
TestLinearLayout linearLayout;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

myButton = (TestButton) findViewById(R.id.myButton);
linearLayout = (TestLinearLayout) findViewById(R.id.myLayout);

myButton.setOnClickListener(this);
myButton.setOnTouchListener(this);

linearLayout.setOnClickListener(this);
linearLayout.setOnTouchListener(this);
}

@Override
public void onClick(View v) {
Log.d("MainActivity", "onClick " + v);
}

@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("MainActivity", "onTouch action = "+event.getAction() + v);
return false;
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("MainActivity", "dispatchTouchEvent action = " + ev.getAction());
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("MainActivity", "onTouchEvent action = " + event.getAction());
return super.onTouchEvent(event);
}
}

然后我们运行项目,点击button按钮,会得到如下的信息:

touch-button

点击button以外的地方,会得到如下的信息:

touch-layout

建议使用模拟器来运行点击事件。如果用真机的话,会触发大量的ACTION_MOVE事件。

事件分发的流程就出来了,那么为什么会是这样的呢?注意到事件都是从MainActivitydispatchTouchEvent()方法开始调用的,那我们就从这个方法开始着手看代码吧。

Activity 的事件分发

事件传递到Activity后,第一个触发的方法是dispatchTouchEvent(),我们看一看这个方法的源码:

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

好简单的代码,最喜欢看到这种“小清新”风格的代码了。

首先判断MotionEvent是不是ACTION_DOWN,如果是的话,执行onUserInteraction()方法,然后判断getWindow().superDispatchTouchEvent(ev)是否为true,如果为true,就不会执行onTouchEvent()方法,如果为false则执行onTouchEvent()方法。

onUserInteraction()

这个方法只有当ACTION_DOWN时才会触发, 那么这个方法是干嘛的?我们点进去看,会发现啥也没有。这个方法是空方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* <p>All calls to your activity's {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
* <p>Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}

还好有注释,不然都不知道这个方法是用来干嘛的了。当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。所以它会用在屏保应用上,因为当你触屏机器 就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保满足此需求。

getWindow().superDispatchTouchEvent(ev)

这个方法是干嘛的呢?我们点进superDispatchTouchEvent()看看:

1
2
3
4
5
6
7
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);

意思大概是说,这个方法会在Dialog等界面中使用到,开发者不需要实现或者调用它。

纳尼?这样就把我们打发了?不带这样玩的啊…

我们注意到superDispatchTouchEvent()方法是getWindow()调用的,getWindow()方法返回的是一个Window对象。我们在Window类的说明中可以看到一下内容:

1
2
3
4
5
6
7
8
9
10
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/

相信大家的英语应该比我好,我就不翻译了,注意到这句话:The only existing implementation of this abstract class is android.view.PhoneWindow,这是说PhoneWindowWindow的唯一实现类。那么我们就可以在PhoneWindow类中看一下superDispatchTouchEvent()方法究竟干了什么。

PhoneWindow类中,我们可以看到如下代码:

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

很简单,mDecor对象调用了superDispatchTouchEvent()方法。那么mDecor对象又是什么?

跳转到mDecor的声明,代码如下:

1
2
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

发现原来mDecorDecorView的实例。等等,注释里面说,DecorView是视图的顶层view?

再跳转到DecorView类的定义处,发现这么一行代码:

1
2
3
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
......

现在我们清楚了,DecorView继承自FrameLayout,是我们编写所有的界面代码的父类。
然后我们看看mDecor.superDispatchTouchEvent()这个方法干了什么,也就是在DecorView类中,superDispatchTouchEvent()方法的内容:

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

嗯? 看不懂?注意,刚才我们已经知道了,DecorView是继承自FrameLayout,那么它的父类就应该是ViewGroup了,而super.dispatchTouchEvent(event)方法,其实就应该是ViewGroupdispatchTouchEvent()方法,不信?你可以点进去看看。

现在回到最开始的地方:在Activity 中:

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

其中getWindow().superDispatchTouchEvent(ev)的意义大概就清楚了,就是说,如果视图顶层的ViewGroup-DecorView类-的dispatchTouchEvnent()方法返回true的话,就不会执行onTouchEvent()方法了。

那么,问题来了,ViewGroup中的dispatchTouchEvent()方法什么时候返回true,什么时候返回false呢?

ViewGroup的事件分发

先看一下源码:

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
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}

// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}

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

// 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;
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;
if (!canceled && !intercepted) {

// If the event is targeting accessiiblity 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 = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(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 (!canViewReceivePointerEvents(child)
|| !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();
}

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

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

是不是瞬间感觉头大了?和Activity中dispatchTouchEvent()简洁优美的代码完全是两个风格,面对这堆庞然大物,我们要有目的性的,策略性的解决它!

回顾一下我们目标:ViewGroup中的dispatchTouchEvent()方法什么时候返回true,什么时候返回false

然后看一下ViewGroup方法中dispatchTouchEvent()方法返回的是handled的值,也就是我们要特别关注该方法中会改变handled值的语句。

第12行代码初始化了handled的值,默认为false

第13行的if (onFilterTouchEventForSecurity(ev))判断囊括了dispathcTouchEvent()的几乎所有的方法。也就是说,如果onFilterTouchEventForSecurity(ev)返回为true的话,就表示可以分发该触摸事件,如果返回为false,则不分发事件。查看一下onFilterTouchEventForSecurity(ev)的方法:

1
2
3
4
5
6
7
8
9
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
  1. FILTER_TOUCHES_WHEN_OBSCUREDandroid:filterTouchesWhenObscured属性所对应的位。android:filterTouchesWhenObscured是true的话,则表示其他视图在该视图之上,导致该视图被隐藏时,该视图就不再响应触摸事件。
  2. MotionEvent.FLAG_WINDOW_IS_OBSCUREDtrue的话,则表示该视图的窗口是被隐藏的。

然后在第18行开始判断,如果是ACTION_DOWN事件的话,就会触发cancelAndClearTouchTargets(ev);resetTouchState();方法,在resetTouchState()方法中,有一个clearTouchTargets();方法,这个方法将mFirstTouchTarget设置为null。为什么要关心这个呢?很快你就知道答案了。

第27行创建了一个intercepted的布尔变量,看名字就知道是记录是否拦截的标志了。第28行有一个判断:actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null,刚才我们知道了mFirstTouchTarget是为null的,也就是说,如果是ACTION_DOWN的话,就会直接进入这个方法块中。

然后第30行判断是否设置了FLAG_DISALLOW_INTERCEPT标志,如果设置了,则disallowIntercepttrue(禁止拦截判断), intercepted直接设置为false。

对于这个FLAG_DISALLOW_INTERCEPT标志,其实有两种方法可以设置,第一种自然是通过ViewGroup设置Flag来,第二种就是通过代码,调用ViewGroup中的requestDisallowInterceptTouchEvent()方法设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}

if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}

// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}

可以看到,其实通过代码的方式最后还是将ViewGroup设置FLAG_DISALLOW_INTERCEPTflag。在我们的代码中,使用getParent().requestDisallowInterceptTouchEvent(true);可以剥夺父view 对除了ACTION_DOWN以外的事件的处理权。

为什么是ACTION_DOWN以外的呢?
其实在第23行代码resetTouchState()的方法中,有一行代码:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;也就是每次来到ViewGroupdispatchTouchEvent()方法里面时,都会重置这个flag。因此,设置FLAG_DISALLOW_INTERCEPTflag并不能影响ViewGroupACTION_DOWN的处理。

好了,扯远了,调用onInterceptTouchEvent()方法,然后将结果赋值给intercepted。那就来看下ViewGroup与众不同与View特有的onInterceptTouchEvent方法:

1
2
3
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}

很简单,直接返回false。但是我们可以重写这个方法,也就是说,如果我们在ViewGroup中重写了onInterceptTouchEvent()方法,并且让它返回true。那么intercepted也为true,表示拦截事件。

第57行,如果既不是ACTION_CANCEL,也不拦截事件,那么就会进行下面的判断:

1
2
3
if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
......
}

在第79行判断了childrenCount != null,然后从子View的最后一个元素开始往前遍历。

第106行判断判断子view是否能接受点击事件。

判断完了之后在137行有一个newTouchTarget = addTouchTarget(child, idBitsToAssign);语句,查看addTouchTarget()方法,会发现里面有一行代码是这样的:mFirstTouchTarget = target;这里就是将mFirstTouchTarget赋值的地方。

也就是说,当子View存在,并将事件传递给了子View后,mFirstTouchTarget就不为null了,
这个时候,如果在传入ACTION_MOVEACTION_UP事件进入ViewGoup的dispatchTouchEvent()方法后,由于不会调用cancelAndClearTouchTargets(ev)方法,mFirstTouchTarget也就不为null!!!这种情况需要特别注意,将直接影响代码的分析。

如何派发事件呢?注意到第121行有一个判断:if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign),这个dispatchTransformedTouchEvent()在后面还会出现,可以看一下里面的代码。由于代码比较长,而且我们关心的是和child有关的代码,抽象出来,就如同下面的判断:

1
2
3
4
5
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}

也就是说,如果子view为空,则调用ViewGroup自身的dispatchTouchEvent()方法,如果不为空,则调用子View的dispatchTouchEvent()方法。这里就是派发事件的地方。

回到我们分析的地方。在第121行,由于判断了子View非空,则会调用子View的dispatchTouchEvent()方法,该子View既可能是View,也可能是ViewGroup。而如果没有子View,就会在164行调用super.dispatchTouchEvent()方法(其实也是View的dispatchTouchEvent()方法,因为ViewGroup的父类就是View,然而View的dispatchTouchEvent()方法和ViewGroup的那个方法是有区别的)。

现在,让我们总结一下:

  • ViewGroup执行dispatchTouchEvent()方法后,会执行onInterceptTouchEvent()方法。
  • 如果我们重写onInterceptTouchEvent()方法,并且返回true,那么就会调用父类的dispatchTouchEvent()方法。
  • 如果我们没有重写onInterceptTouchEvent()方法,或者重写了该方法但是返回了false。如果有子View,则会调用子View的dispatchTouchEvent()方法,如果没有子View,则调用父类(View)的dispatchTouchEvent()方法。

看来无论怎么样都会触发到View中的dispatchTouchEvent()方法,而且对于我们最开始的问题,也是和View中的dispatchTouchEvent()方法的返回值有直接的关系。那么,该方法里面又是怎样一番美景呢?

View的事件分发

那么我们看一看View里面的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
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
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)) {
//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;
}
}

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

第26行判断是否为ACTION_DOWN,如果是,则停止滚动。

第31行通过onFilterTouchEventForSecurity()方法判断当前View是否被覆盖。

第34行开始有一个判断语句:if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))
这个判断语句比较重要,下面逐个分析:

  • li 对象肯定不为空。因为li对象由mListenerInfo赋值,而关于mListenerInfo有下面的代码存在:
1
2
3
4
5
6
7
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
  • li.mOnTouchListener 是否为空呢?这个就要看我们是否为View设置了OnTouchListener了,如果设置了,就不为空;没设置就为空。
  • (mViewFlags & ENABLED_MASK) == ENABLED这个判断当前的View是否为ENABLE,默认都是enable。
  • li.mOnTouchListener.onTouch(this, event)) 这个就是判断OnTouchLinstener的返回了。

如果上面4点都为true,则34行的判断语句也为true。这时候View中的dispatchTouchEvent()返回值也为true。

到这里我们就能推导出一些结论性的东西了:View的dispatchTouchEvent()方法触发后,如果该View为ENABLE,并设置了OnTouchLisener且返回为true。那么该View的dispatchTouchEvent()返回true。

如果该View没有设置OnTouchListener()或者OnTouchListener()返回false。那么就会执行到第40行做一个判断onTouchEvent(event)。那么onTouchEvent()什么时候返回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
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
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);
}
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;
}

这个方法的代码和ViewGroup的dispatchTouchEvent()源码的长度有得一拼了。

第7行有一个判断,如果该View为DISABLE,返回值就由13行的判断决定:如果CLICKABLELONG_CLICKABLECONTEXT_CLICKABLE有一个为true,则返回true。(默认情况下,LONG_CLICKABLE为false)

其实我们可以发现,只要进了第7行判断的代码,该onTouchEvent()方法就返回true,否则就返回false。这一点很重要。

如果View是ENABLE的,那么就会进入第24行的判断。在ACTION_UP事件里面,有一个方法performClick()我们看一下它的源码:

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

在第4行有一个判断。之前已经知道了li不为空了。那么li.mOnClickListener呢?我们看一下li.mOnClickListener赋值的地方:

1
2
3
4
5
6
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}

setOnClickLisnter()!!!多么熟悉的方法!!!也就是说,如果我们为View重写了setOnClickLinstener()方法的话,performClick()就返回true,否则返回false。

好了,结合上面的分析,我们可以得出关于View事件分发的一些结论:

  • 如果设置了OnTouchListener()且返回true。View的事件流程就完成了,不会执行到OnClickLinstener()方法。
  • 如果没有设置OnTouchLinstener()或者设置了OnTouchListener()且返回false。则View会执行OnClickLinster()方法。

如果View中的dispatchTouchEvent()返回了false,结合我们之前对于Activity的相关分析,就会调用到Activity的onTouchEvent()方法。

终于结束了一个事件的轮回,不容易啊。

让我喝口茉莉花茶,再聊聊剩下的。

小结

结合之前对ActivityViewGroup的相关分析,我们可以得出事件分发的大致流程:

  • 事件分发是从Activity开始,传递到Window,再传递给视图的顶级View(一般是View的子类ViewGroup)。然后顶级View再派发事件。
  • 事件传递到ViewGroup之后,会首先调用dispatchTouchEvent()方法,然后执行onInterceptTouchEvent()方法。由于ViewGroup中的该方法默认返回false,所以ViewGroup默认不拦截事件。如果我们重写了onInterceptTouchEvent()方法并返回了true,那么该事件就被消耗掉了,不会往下派发了。否则传递到子View(如果有),或者调用View的onInterceptTouchEvent()方法。
  • 当事件传递到View之后(这里的View包含ViewGroup),会首先调用dispatchTouchEvent()方法。在该方法内会调用OnTouchListener(),如果设置了OnTouchListener()且返回true。View的事件流程就完成了,不会执行到OnClickLinstener()方法。如果没有设置OnTouchLinstener()或者设置了OnTouchListener()且返回false。则View会执行OnClickLinster()方法。至此,一个事件派发结束。
  • 如果事件在ViewGroup中就被拦截了(onInterceptTouchEvent()方法被重写并返回了true),或者事件在View中的dispatchTouchEvent()方法返回了false。(有好多种可能的情况使得该方法返回false,详见上面的分析),就会调用到Activity中的onTouchEvent()方法。

也就是说,在整个流程中会涉及到以下三个方法:

  1. dispatchTouchEvent() 用来分发事件。如果事件传递到了某个view,则该view的dispatchTouchEvent()方法肯定会调用到。而其返回值受到下面两个方法的影响。
  2. onInterceptTouchEvent() 只有ViewGroup中存在该方法,用来决定是否拦截某个事件。
  3. onTouchEvent() 在dispatchTouchEvent()方法中会调用到这个方法。用来处理点击事件。

当然,在分析的过程中我们也得出了一些比较有意思的结论,比如:

  • View的ENABLE属性是否为true并不会对onTouchEvent()的默认返回值有影响。只要clickablelongclickable或者contextclickable有一个为true,那么onTouchEvent()就返回true。
  • 我们可以通过requestDisallowInterceptTouchEvent()方法在子View中控制父View的事件是否传递,但是对ACTION_DOWN事件除外。
  • View没有onInterceptTouchEvent()方法,而ViewGroup中有该方法。这是二者的区别之一。

大概就是这些了,欢迎交流。

以后再看源码一定尽量选低版本的源码看了,嗯。

如果觉得文章对你有帮助,请我喝杯可乐吧