聊聊 "事件分发机制"

核心源码(Android_10.0)

关键类 路径
Activity.java frameworks/base/core/java/android/app/Activity.java
DecorView.java frameworks/base/core/java/com/android/internal/policy/DecorView.java
PhoneWindow.java frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
View.java frameworks/base/core/java/android/view/View.java
ViewGroup.java frameworks/base/core/core/java/android/view/ViewGroup.java
Window.java frameworks/base/core/java/android/view/Window.java

一、基础认知

Android View 虽然不是四大组件,但其并不比四大组件的地位低。

View 的核心知识点 “事件分发机制” 则是不少刚入门童鞋的拦路虎(1、项目中处处遇到事件分发机制;2、面试必备)。

1.1 事件分发的 “对象”

点击事件(Touch Event)

> 定义

当用户触摸屏幕时(View 或 ViewGroup 派生的控件),将产生点击事件(Touch Event),Touch 事件的相关细节(发生触摸的位置、时间等)被封装成了 MotionEvent 对象。

> 事件类型

事件类型 具体动作
MotionEvent.ACTION_DOWN 按下 View(所有事件的开始)
MotionEvent.ACTION_UP 抬起 View(与 DOWN 对应)
MotionEvent.ACTION_MOVE 滑动 View
MotionEvent.ACTION_CANCEL 结束事件(非人为原因)

> 事件序列

所谓事件序列就是指:从手指接触屏幕至手指离开屏幕,这个过程产生的一系列事件。

一般情况下,事件列都是以 DOWN 事件开始、UP 事件结束,中间有无数的 MOVE 事件

1.2 事件分发的 “本质”

将点击事件传递到某个具体的 View 并处理的整个过程,事件传递的过程 = 分发过程

1.3 事件分发的 “对象”

Activity、ViewGroup、View

1.4 事件分发的 “顺序”

Activity -> ViewGroup -> View

1.5 事件分发的 “方法”

三个重要方法: dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent(),先简单看下,我们下面会细致分析。

dispatchTouchEvent(事件分发):当监听到有触发事件时,首先由 Activity 进行捕获,然后事件就进入事件分发的流程。Activity 本身没有事件拦截,从而将事件传递给最外层的 View 的 dispatchTouchEvent(MotionEvent ev),该方法将对事件进行分发。

返回值:表示 是否消费了当前事件。可能是 View 本身的 onTouchEvent() 消费,也可能是子 View 的 dispatchTouchEvent() 中消费。返回 true 表示事件被消费,本次的事件终止。返回 false 表示 View 以及子 View 均没有消费事件,将调用父 View 的 onTouchEvent()。

onInterceptTouchEvent(事件拦截):当一个 ViewGroup 在接到 MotionEvent 事件序列时候,首先会调用此方法判断是否需要拦截。特别注意,这是 ViewGroup 特有的方法,View 并没有拦截方法。

返回值: 是否拦截事件传递,返回 true 表示拦截了事件,那么事件将不再向下分发而是调用 View 本身的 onTouchEvent()。返回 false 表示不做拦截,事件将向下分发到子 View 的 dispatchTouchEvent()。

onTouchEvent(事件响应):真正对 MotionEvent 进行处理或者说消费的方法,在 dispatchTouchEvent() 中进行调用。

返回值:返回 true 表示 事件被消费,本次的事件终止。返回 false 表示事件没有被消费,将调用父 View 的 onTouchEvent 方法,直到返回 true 为止。

我们可以做个总结:

    ✎  Activity 的点击事件事实上是调用它内部的 ViewGroup 的点击事件,可以直接当成 ViewGroup 处理。

    ✎  ViewGroup 的相关事件方法有三个:onInterceptTouchEventdispatchTouchEventonTouchEvent

    ✎  View 的相关事件方法只有两个:dispatchTouchEventonTouchEvent


要想充分理解 Android 事件分发机制,本质上是要理解以下三个部分:

Activity - Touch 事件分发     ✯ ViewGroup - Touch 事件分发      ✯ View - Touch 事件分发

二、Touch 事件分发 – Activity

当一个点击事件发生时,事件最先传到 Activity 的 dispatchTouchEvent 方法。

2.1 Activity.dispatchTouchEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// frameworks/base/core/java/android/app/Activity.java

public class Activity extends ContextThemeWrapper ... {

public boolean dispatchTouchEvent(MotionEvent ev) {
// 1 一般事件列开始都是 DOWN 事件,即按下事件,所以此处基本是 true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) { // 2
return true;
}
return onTouchEvent(ev); // 3
}

}

我们发现,Activity 的 dispatchTouchEvent 主要涉及三个方法:onUserInteractionsuperDispatchTouchEventonTouchEvent

2.2 Activity.onUserInteraction

1
2
3
4
5
6
7
8
9
10
11
12
13
// frameworks/base/core/java/android/app/Activity.java

public class Activity extends ContextThemeWrapper ... {

/**
* 说明:
* a. 该方法为空方法,主要实现屏保功能
* b. 当此 activity 在栈顶时,触屏点击按 home,back,menu 键等都会触发此方法
*/
public void onUserInteraction() {
}

}

2.3 PhoneWindow.superDispatchTouchEvent

接下来,我们看看 superDispatchTouchEvent 方法:

1
2
3
4
5
6
7
8
9
// frameworks/base/core/java/android/app/Activity.java

/**
* getWindow():获取 Window 类的对象,Window 类是抽象类,
* 其唯一实现类是 PhoneWindow 类;即此处的 Window 类对象 = PhoneWindow 类对象。
*/
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}

Window 类的 superDispatchTouchEvent() 是一个抽象方法,由子类 PhoneWindow 类实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

public class PhoneWindow extends Window implements MenuBuilder.Callback {
... ...

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

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
... ...
}

我们发现,ActivitysuperDispatchTouchEvent 方法最终会走到 DecorViewsuperDispatchTouchEvent 方法。

2.4 DecorView.superDispatchTouchEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// frameworks/base/core/java/com/android/internal/policy/DecorView.java

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {

/**
* public class DecorView extends FrameLayout,
* -- DecorView 继承自 FrameLayout,是所有界面的父类。
* public class FrameLayout extends ViewGroup,
* -- FrameLayout 是 ViewGroup 的子类,故 DecorView 的间接父类是 ViewGroup。
*/

public boolean superDispatchTouchEvent(MotionEvent event) {
// 调用父类的方法:ViewGroup 的 dispatchTouchEvent(),
// 即:将事件传递到 ViewGroup 去处理,我们在 ViewGroup 的事件分发机制继续讨论。
return super.dispatchTouchEvent(event);
}

}

所以不难发现,ActivitydispatchTouchEvent 方法最终会走到 ViewGroupdispatchTouchEvent 方法。

2.5 Activity.onTouchEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// frameworks/base/core/java/android/app/Activity.java

public class Activity extends ContextThemeWrapper ... {

public boolean onTouchEvent(MotionEvent event) {
// 一般返回 true,除非 Touch 事件在 Window 边界外,所以这边我们不再继续跟踪。
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}

return false;
}

}

2.6 Activity 事件分发小结

看下 Activity 对点击事件的处理流程图:

Activity 事件分发流程

至此,分析完 Activity 对点击事件的分发机制处理流程,我们不难发现,Activity 的事件走到了 ViewGroup 进行处理,那么接下来就是分析 ViewGroup 对点击事件的分发机制了。


三、Touch 事件分发 – ViewGroup

3.1 Demo

我们先通过一个 Demo 来看看 ViewGroup 对点击事件的处理。

Layout 代码:

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
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/my_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<Button
android:id="@+id/button_01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button_01"
tools:layout_editor_absoluteX="94dp"
tools:layout_editor_absoluteY="106dp" />

<Button
android:id="@+id/button_02"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button_02"
tools:layout_editor_absoluteX="94dp"
tools:layout_editor_absoluteY="211dp" />
</android.support.constraint.ConstraintLayout>

Activity 代码:

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
package com.example.marco.myapplication;

import android.support.constraint.ConstraintLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

private Button button_01;
private Button button_02;
private ViewGroup myLayout;


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

button_01 = (Button) findViewById(R.id.button_01);
button_02 = (Button) findViewById(R.id.button_02);
myLayout = (ConstraintLayout) findViewById(R.id.my_layout);

// 1. ViewGroup: myLayout 布局设置监听事件
myLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "点击了ViewGroup");
}
});

// 2. View: button_01 设置监听事件
button_01.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "点击了button_01");
}
});

// 3. View: button_02设置监听事件
button_02.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "点击了button_02");
}
});

}
}

测试结果:

1
2
3
09-26 16:17:51.877 16250 16250 D TAG : 点击了button_01     // 点击按钮 button_01
09-26 16:17:53.875 16250 16250 D TAG : 点击了button_02 // 点击按钮 button_02
09-26 16:17:54.758 16250 16250 D TAG : 点击了ViewGroup // 点击空白处

结果说明:

  • 点击 Button 时,执行 Button.onClick(),但 ViewGroup_Layout 注册的 onClick() 不会执行;
  • 只有点击空白区域时,才会执行 ViewGroup_Layout 的 onClick() 方法。

结论:Button 的 onClick() 将事件消费掉了,因此事件不会再继续向下传递。

那么这里提出一个疑问,这样的结果是不是说明 Touch 事件是先传递到 View,再传递到 ViewGroup 的么?但从前面的分析,我们知道是从 Activity 传递到 ViewGroup 进行处理,这不是矛盾么?别急,我们通过源码来看看怎么回事。

3.2 ViewGroup.dispatchTouchEvent

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
// frameworks/base/core/java/android/view/ViewGroup.java

// First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

public boolean dispatchTouchEvent(MotionEvent ev) {
... ...

boolean handled = false; // 这个变量用于记录事件是否被处理完
if (onFilterTouchEventForSecurity(ev)) { // 过滤掉一些不合法的事件
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

// 判断是不是 Down 事件,如果是的话,就要做初始化操作
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev); // 如果是 Down 事件,就要清空掉之前的状态
resetTouchState();
}

final boolean intercepted; // 是否拦截事件
// 如果当前是 Down 事件,或者已经有处理 Touch 事件的目标了
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
/**
* disallowIntercept:是否禁用事件拦截的功能,默认为 false,我们也可以通过调用
* requestDisallowInterceptTouchEvent() 方法对这个值进行修改。
*/
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) { // 默认为 false 所以会走以下流程
// 每次事件分发时,都需调用 onInterceptTouchEvent() 询问是否拦截事件
intercepted = onInterceptTouchEvent(ev); // 核心方法一
// 重新恢复 Action,以免 Action 在上面的步骤被人为地改变了
ev.setAction(action); // restore action in case it was changed
} else {
// 如果禁用了事件拦截功能,则 intercepted 肯定为 false
intercepted = false;
}
} else {
// 如果说事件已经初始化过了,并且没有子 View 被分配处理,
// 那么就说明,这个 ViewGroup 已经拦截了这个事件。
intercepted = true;
}

if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}

// Check for cancelation,标志着取消事件.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;

// 如果需要(不取消,也没有被拦截),那么在触摸 Down 事件的时候更新触摸目标列表
// split:代表当前的 ViewGroup 是不是支持分割 MotionEvent 到不同的 View 当中
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null; // 新的触摸对象
boolean alreadyDispatchedToNewTouchTarget = false; // 是否把事件分配给了新的触摸

★ ★ ★ ★ ★ ★ ★ ★ 重点方法 ★ ★ ★ ★ ★ ★ ★ ★
// 如果事件不是取消事件,也没有拦截,那么进入此函数
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;

// 如果是个全新的 Down 事件,或者是有新的触摸点,或者是光标来回移动事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

// 事件的索引,Down 事件的 index:0
final int actionIndex = ev.getActionIndex(); // always 0 for down
// 获取分配的 ID 的 bit 数量
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;

// 清理之前触摸这个指针标识,以防它们的目标变得不同步
removePointersFromTouchTargets(idBitsToAssign);

final int childrenCount = mChildrenCount;
// 如果新的触摸对象为 null & 当前 ViewGroup 有子元素
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;

// 通过 for 循环,遍历了当前 ViewGroup 下的所有子 View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);

if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
... ... // 无关紧要的代码我们这边暂且省略

// 核心方法二:派发事件到子 View 处理
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
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;
// 如果子 View 处理了事件,则 break,ViewGroup 不再处理事件(事件被拦截)
// 例子说的 Button.onClick() 处理了之后,ViewGroup.onClick() 不会再执行
break;
}

ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
... ...

}
}
... ...

}
return handled;
}

}

其实 ViewGroup 的 dispatchTouchEvent 处理流程,我们只需要关注两个重点方法:onInterceptTouchEventdispatchTransformedTouchEvent

3.3 ViewGroup.onInterceptTouchEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// frameworks/base/core/java/android/view/ViewGroup.java

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

public boolean onInterceptTouchEvent(MotionEvent ev) {
// 一堆判断,我们只需要知道一点,一般 Touch 事件默认返回 false
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}

}

3.4 ViewGroup.dispatchTransformedTouchEvent

dispatchTransformedTouchEvent 方法的作用,主要就是把事件下发给子 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
// frameworks/base/core/java/android/view/ViewGroup.java

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
... ...

final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
// 调用父类的 dispatchTouchEvent 方法,也就是 ViewGroup.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);

// dispatchTouchEvent()
handled = child.dispatchTouchEvent(transformedEvent);
}

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

}

你有没有发现?当存在子 View 的时候,会调用 View.dispatchTouchEvent 方法,如果没有则会向上调用 super.dispatchTouchEvent 方法。

       1、Android 事件分发是先传递到 ViewGroup,再由 ViewGroup 传递到 View 的。

       2、在 ViewGroup 中可以通过 onInterceptTouchEvent 方法对事件传递进行拦截,onInterceptTouchEvent 方法返回 true 代表不允许事件继续向子 View 传递,返回 false 代表不对事件进行拦截,默认返回 false。

       3、子 View 中如果将传递的事件消费掉,ViewGroup 中将无法接收到任何事件。

3.5 ViewGroup 事件分发小结

看下 ViewGroup 对点击事件的处理流程图:

ViewGroup 事件分发流程


四、Touch 事件分发 – View

这边我们先给出一个说明(结论):

其实,只要你触摸了任何控件,就一定会调用该控件的 dispatchTouchEvent 方法!

严格一点来说:当你点击了某个控件,首先会去调用该控件所在布局dispatchTouchEvent 方法,然后在布局dispatchTouchEvent 方法中找到被点击的相应控件,再去调用该控件dispatchTouchEvent方法。

4.1 Demo

我们先通过一个 Demo 来看看 View 对点击事件的处理,这边我们只设置一个简单的 Button。

Layout代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/my_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<Button
android:id="@+id/button_01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button_01"
tools:layout_editor_absoluteX="94dp"
tools:layout_editor_absoluteY="106dp" />

</LinearLayout>

Activity代码:

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
package com.example.marco.myapplication;

import android.support.constraint.ConstraintLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;

public class MainActivity extends AppCompatActivity {

private Button button_01;


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

button_01 = (Button) findViewById(R.id.button_01);

/**
* 结论验证 1:在回调 onTouch() 里返回 false
*/

// 1. 通过 OnTouchListener() 复写 onTouch(),从而手动设置返回 false
button_01.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "run onTouch(), action:" + event.getAction());
return false;
}
});

// 2. 通过 OnClickListener()为控件设置点击事件,
// 为 mOnClickListener 变量赋值(即不为空),从而往下回调 onClick()
button_01.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "run onClick()");
}
});

/**
* 结论验证 2:在回调 onTouch() 里返回 true
*/

// 1. 通过 OnTouchListener()复写 onTouch(),从而手动设置返回 true
button_01.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "run onTouch(), action:" + event.getAction());
return true;
}
});

// 2. 通过 OnClickListener()为控件设置点击事件,
// 为 mOnClickListener 变量赋值(即不为空),从而往下回调 onClick()
button_01.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "run onClick()");
}
});

}
}

测试结果:

1
2
3
4
5
6
7
// 通过 OnTouchListener() 复写 onTouch(),从而手动设置返回 false
09-26 18:14:19.299 23350 23350 D TAG : run onTouch(), action:0 // ACTION_DOWN
09-26 18:14:19.327 23350 23350 D TAG : run onTouch(), action:2 // ACTION_MOVE
09-26 18:14:19.343 23350 23350 D TAG : run onTouch(), action:2 // ACTION_MOVE
09-26 18:14:19.383 23350 23350 D TAG : run onTouch(), action:2 // ACTION_MOVE
09-26 18:14:19.384 23350 23350 D TAG : run onTouch(), action:1 // ACTION_UP
09-26 18:14:19.385 23350 23350 D TAG : run onClick()
1
2
3
4
5
// 通过 OnTouchListener() 复写 onTouch(),从而手动设置返回 true
09-26 18:16:29.758 23847 23847 D TAG : run onTouch(), action:0 // ACTION_DOWN
09-26 18:16:29.773 23847 23847 D TAG : run onTouch(), action:2 // ACTION_MOVE
09-26 18:16:29.856 23847 23847 D TAG : run onTouch(), action:2 // ACTION_MOVE
09-26 18:16:29.858 23847 23847 D TAG : run onTouch(), action:1 // ACTION_UP

       ✒ 1、我们发现 onTouch() 是优先于 onClick() 执行的;
       ✒ 2、onTouch() 方法是有返回值的,如果返回 true,事件被消费,那么 onClick() 将不再执行;

4.2 dispatchTouchEvent

案例看了,但是为什么会出现上面的结果,还是需要从源码角度去深入分析。首先,我们需要知道的是:只要你触摸到了任何一个控件,就一定会调用该控件的 dispatchTouchEvent 方法。所以,当我们点击按钮的时候,就会去调用 Button 类里的 dispatchTouchEvent方法,可是你会发现 Button 类里并没有这个方法,那么就到它的父类 TextView 里去找一找,但是 TextView 里也没有这个方法,继续往上找,最终我们在 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
// 调用 View.dispatchTouchEvent() 方法
public boolean dispatchTouchEvent(MotionEvent event) {

... ...

if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}

ListenerInfo li = mListenerInfo;

★ ★ ★ ★ ★ ★ ★ ★ 重点方法 ★ ★ ★ ★ ★ ★ ★ ★
/*
* 注意:只有以下3个条件都为真,dispatchTouchEvent() 才返回 true
* 1. li != null & li.mOnTouchListener != null
* 2. (mViewFlags & ENABLED_MASK) == ENABLED
* 3. mListenerInfo.mOnTouchListener.onTouch(this, event)
*/
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
★ ★ ★ ★ ★ ★ ★ ★ 重点方法 ★ ★ ★ ★ ★ ★ ★ ★

// 如果上面没有返回 true,那么执行 onTouchEvent()
if (!result && onTouchEvent(event)) { // 下面再分析
result = true;
}
}
... ...

return result;
}

我们来分析一下重点方法中提到的几个关键条件:

条件 1:li != null & li.mOnTouchListener != null

mOnTouchListener 这个变量是在哪里赋值的呢?

1
2
3
4
5
6
7
8
/**
* 条件 1:mListenerInfo.mOnTouchListener != null
* 说明:mOnTouchListener 变量在 View.setOnTouchListener() 方法里赋值
*/
public void setOnTouchListener(OnTouchListener l) {
// 所以,只要我们给控件注册了 Touch 事件,mOnTouchListener 就一定被赋值(不为空)
getListenerInfo().mOnTouchListener = l;
}

条件 2:(mViewFlags & ENABLED_MASK) == ENABLED

1
2
3
4
5
6
/**
* 条件 2:(mViewFlags & ENABLED_MASK) == ENABLED
* 说明:
* a. 该条件是判断当前点击的控件是否 enable
* b. 一般 View 默认 enable,故该条件恒定为 true
*/

条件 3:li.mOnTouchListener.onTouch(this, event)

mListenerInfo.mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册 touch 事件时的 onTouch 方法。

也就是说如果我们在 onTouch 方法里返回 true,就会让这三个条件全部成立,从而整个方法直接返回 true。如果我们在 onTouch 方法里返回 false,就会再去执行 onTouchEvent(event) 方法。

1
2
3
4
5
6
7
8
9
10
11
 /**
* 条件 3:mOnTouchListener.onTouch(this, event)
* 说明:回调控件注册 Touch 事件时的 onTouch()
*/
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// return true;
// return false;
}
});

4.3 onTouchEvent

接下来,我们看看 onTouch() 返回 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
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
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;

... ...

// 若该控件可点击,则进入 switch 判断中
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
// a. 若当前的事件 = 抬起 View
case MotionEvent.ACTION_UP:
... ...
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
... ...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();

if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick(); // 重点分析函数
}
}
}
...
}
break;

// b. 若当前的事件 = 按下 View
case MotionEvent.ACTION_DOWN:
... ...
break;

// c. 若当前的事件 = 结束事件(非人为原因)
case MotionEvent.ACTION_CANCEL:
... ...
break;

// d. 若当前的事件 = 滑动 View
case MotionEvent.ACTION_MOVE:
... ...
break;
}

// 若该控件可点击,就一定返回true
return true;
}

// 若该控件不可点击,就一定返回false
return false;
}

4.4 performClick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
/*
* 只要我们通过 setOnClickListener() 为控件 View 注册 1 个点击事件,那么就会给 li.mOnClickListener
* 变量赋值(即不为空),则会往下回调 onClick()。
*/
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this); // 执行 onClick()
result = true;
} else {
result = false;
}
... ...

return result;
}

根据以上源码的分析,从原理上解释了我们前面 Demo 的运行结果。

这边我们来看个很有意思的现象:如果我们把 Button 换成 ImageView,然后给它注册一个 Touch 事件,结果会怎样?

1
2
3
4
5
6
7
imageView.setOnTouchListener(new OnTouchListener() {  
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "run onTouch(), action:" + event.getAction());
return false;
}
});

运行一下程序,点击ImageView,你会发现结果如下:

1
2
// 通过 OnTouchListener() 复写 onTouch(),从而手动设置返回 true
09-26 18:16:29.758 23847 23847 D TAG : run onTouch(), action:0 // ACTION_DOWN

ACTION_DOWN 执行完后,后面的一系列 action都不会得到执行了!怎么会出现这个结果?

因为 ImageView 和按钮不同,它是默认不可点击的,因此在 onTouchEvent 的源码中进行 if 判断 的时候是进不去的,所以直接返回了 false,也就导致后面其它的 action 都无法执行了。

关于事件分发有个重要的知识点:就是 Touch 事件的层级传递。我们都知道如果给一个控件注册了 touch 事件,每次点击它的时候都会触发一系列的 ACTION_DOWN,ACTION_MOVE,ACTION_UP 等事件。这里需要注意,如果你在执行 ACTION_DOWN 的时候返回了 false,后面一系列其它的 action 就不会再得到执行了。简单的说,就是当 dispatchTouchEvent 在进行事件分发的时候,只有前一个 action 返回 true,才会触发后一个 action。

4.5 小结

看下 View 对点击事件的处理流程图:

mZdLrj.png


五、总结

       ✒ 1、Android 事件分发机制主要由【“事件分发” —> “事件拦截” —> “事件响应”】这三步来进行逻辑控制的。

       ✒ 2、ViewGroup 默认不拦截任何事件。

       ✒ 3、onInterceptTouchEvent 返回 true 表示事件拦截,onTouchEvent 返回 true 表示事件消费。

    ✒ 4、点击事件的分发过程如下:dispatchTouchEvent —> onTouchListener 的 OnTouch 方法 —> onTouchEvent —> onClickListener 的 onClick 方法。从而也可以看出 onTouch 是优先于 onClick 执行,事件传递的顺序是先经过 onTouch,再传递到 onClick。

       ✒ 5、Android 中的事件 onClick、onLongClick、onScroll 等,都是由多个 Touch 事件(一个 ACTION_DOWN,多个 ACTION_MOVE,一个 ACTION_UP)组成。

       ✒ 6、子 View 可以通过使用 getParent().requestDisallowInterceptTouchEvent(true) ,阻止 ViewGroup 对其 MOVE 或 UP 事件进行拦截。

       ✒ 7、MotionEvent 对象的四种状态:MotionEvent.ACTION_DOWN:手指按下屏幕的瞬间

                                                                    MotionEvent.ACTION_MOVE:手指在屏幕上移动

                                                                    MotionEvent.ACTION_UP:手指离开屏幕瞬间

                                                                    MotionEvent.ACTION_CANCEL:取消手势

       ✒ 8、点击某个控件,首先会去调用该控件所在布局的 dispatchTouchEvent 方法,然后在布局的 dispatchTouchEvent 方法中找到被点击的相应控件,再去调用该控件的 dispatchTouchEvent方法。

       ✒ 9、如果 View 没有消费 ACTION_DOWN 事件,则之后的 ACTION_MOVE 等事件都不会再接收。

       ✒ 10、事件在从 Activity.dispatchTouchEvent 往下分发的过程中:

如果中间的 ViewGroup 都不拦截,进入最底层的 View 后,由 View.onTouchEvent 处理,如果 View 也没有消费事件,最后会返回到 Activity.onTouchEvent。
如果中间任何一层 ViewGroup 拦截事件,则事件不再往下分发,交由拦截的 ViewGroup 的 onTouchEvent 来处理。

最后,我们用一张图看下“事件分发机制”的处理流程:

mZdjZn.png

参考文献

        1、https://blog.csdn.net/xiashuangyuan1/article/details/72454331
        2、https://www.jianshu.com/p/38015afcdb58