Android事件分发机制



Android事件分发

众所周知,android视图框架由ViewViewGroup构成。ViewGroupView的子类,是所有的布局类的父类,View是所有控件的父类。如下图所示的关系:



Android事件MotionEvent作为用户交互的媒介,包括点击,滑动,长按,拖拽等等。用户每次对屏幕操作动作都由三个最基本的动作构成,它们分别是ACTION_DOWN ACTION_MOVEACTION_UP。这一系列的动作操作被称为一个事件序列。比如说点击事件可以被分解为ACTION_DOWN -> ACTION_UP;滑动操作可以被分解为ACTION_DOWN -> ACTION_MOVE -> ACTION_MOVE -> … ->ACTION_MOVE -> ACTION_UP。可以肯定的是每一个事件序列都是以ACTION_DOWN开始。

我们都知道在手屏幕上呈现的多姿多彩的画面不是由一个或者两个View就能构成的,而是由不同的布局视图ViewGroup和不同的控件View一层一层嵌套形成的视图层级。这么多的View是如何接收到事件的呢?具体的事件又该真正的作用于哪个View呢?

这就需要Android强大框架支持了。为了解决这些问题恰恰需要Android提供一个强大的View事件分发机制。

View源码中提供了两个方法分别是public boolean dispatchTouchEvent(MotionEvent event)public boolean onTouchEvent(MotionEvent event)而在ViewGroup源码中中发现不仅覆写了public boolean dispatchTouchEvent(MotionEvent event)而且还提供了一个public boolean onInterceptTouchEvent(MotionEvent ev)方法。在Activity的源码中也可以找到dispatchTouchEventonTouchEvent两个方法。

通过方法的名字大意上不难看出,它们就是分发事件和消耗事件的关键方法。下面通过实验来观察事件传递的过程。

View事件分发与处理

为了能在dispatchTouchEventonTouchEvent方法中打印日志,这里写两个类来分别继承ButtonFrameLayout,分别命名为CustomButtonCustomLayout

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 CustomLayout extends FrameLayout {
private static final String TAG = "CustomLayout";
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(TAG, " >>>> dispatchTouchEvent >>>> DOWN");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, " >>>> dispatchTouchEvent >>>> UP");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, " >>>> dispatchTouchEvent >>>> MOVE");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(TAG, " >>>> onTouchEvent >>>> DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, " >>>> onTouchEvent >>>> MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, " >>>> onTouchEvent >>>> UP");
break;
}
return super.onTouchEvent(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
public class CustomButton extends Button {
private static final String TAG = "CustomButton";
...
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, ">>>> dispatchTouchEvent >>> DOWN");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, ">>>> dispatchTouchEvent >>> UP");
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, ">>>> onTouchEvent >>> DOWN");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, ">>>> onTouchEvent >>> UP");
break;
}
return super.onTouchEvent(event);
}
}

布局文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<com.icedcap.viewtestdemo.CustomLayout
android:id="@+id/customLayout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.icedcap.viewtestdemo.CustomButton
android:id="@+id/customButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="自定义按钮"/>
</com.icedcap.viewtestdemo.CustomLayout>

Activity中重写dispatchTouchEventonTouchEvent这两个方法,并且为customButtoncustomLayout添加点击事件和触摸事件。

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
public class SjActivity extends Activity implements View.OnClickListener, View.OnTouchListener{
private static final String TAG = "SjActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sj);
findViewById(R.id.customButton).setOnClickListener(this);
findViewById(R.id.customButton).setOnTouchListener(this);
findViewById(R.id.customLayout).setOnClickListener(this);
findViewById(R.id.customLayout).setOnTouchListener(this);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, " >>>> SjActivity >> dispatchTouchEvent >>> DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, " >>>> SjActivity >> dispatchTouchEvent >>> MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, " >>>> SjActivity >> dispatchTouchEvent >>> UP");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, " >>>> SjActivity >> onTouchEvent >>> DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, " >>>> SjActivity >> onTouchEvent >>> MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, " >>>> SjActivity >> onTouchEvent >>> UP");
break;
}
return super.onTouchEvent(event);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.customButton:
Log.d(TAG, ">>>> CustomButton >> onClick!");
break;
case R.id.customLayout:
Log.d(TAG, ">>>> CustomLayout >> onClick!");
break;
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (v.getId() == R.id.customButton) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "CustomButton >>>> onTouch >>>> DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "CustomButton >>>> onTouch >>>> MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "CustomButton >>>> onTouch >>>> UP");
break;
}
} else if (v.getId() == R.id.customLayout){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "CustomLayout >>>> onTouch >>>> DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "CustomLayout >>>> onTouch >>>> MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "CustomLayout >>>> onTouch >>>> UP");
break;
}
}
return false;
}
}

启动这个demo后点击CustomButton得到的日志如下

1
2
3
4
5
6
7
8
9
10
11
05-30 15:32:59.304 2310-2310/com.icedcap.viewtestdemo D/SjActivity: >>>> SjActivity >> dispatchTouchEvent >>> DOWN
05-30 15:32:59.309 2310-2310/com.icedcap.viewtestdemo D/CustomLayout: >>>> dispatchTouchEvent >>>> DOWN
05-30 15:32:59.309 2310-2310/com.icedcap.viewtestdemo D/CustomButton: >>>> dispatchTouchEvent >>> DOWN
05-30 15:32:59.310 2310-2310/com.icedcap.viewtestdemo D/SjActivity: CustomButton >>>> onTouch >>>> DOWN
05-30 15:32:59.310 2310-2310/com.icedcap.viewtestdemo D/CustomButton: >>>> onTouchEvent >>> DOWN
05-30 15:32:59.362 2310-2310/com.icedcap.viewtestdemo D/SjActivity: >>>> SjActivity >> dispatchTouchEvent >>> UP
05-30 15:32:59.362 2310-2310/com.icedcap.viewtestdemo D/CustomLayout: >>>> dispatchTouchEvent >>>> UP
05-30 15:32:59.363 2310-2310/com.icedcap.viewtestdemo D/CustomButton: >>>> dispatchTouchEvent >>> UP
05-30 15:32:59.363 2310-2310/com.icedcap.viewtestdemo D/SjActivity: CustomButton >>>> onTouch >>>> UP
05-30 15:32:59.363 2310-2310/com.icedcap.viewtestdemo D/CustomButton: >>>> onTouchEvent >>> UP
05-30 15:32:59.366 2310-2310/com.icedcap.viewtestdemo D/SjActivity: >>>> CustomButton >> onClick!

从日志可以看出,事件DOWN首先从Activity中的dispatchTouchEvent方法中发出,接着到达视图层级的第一层CustomLayout,再到下一层CustomButtondispatchTouchEvent。由于这一层已经是视图层级的最后一层了,日志中没有看到继续再往下一层分发反而是回调了在Activity中添加的onTouch事件。之后才调用CustomButton中的onTouchEvent方法。这样ACTION_DOWN事件就结束了,紧接着是ACTION_UP事件的传递过程和ACTION_DOWN过程一样,到最后才去回调CustomButtononClick方法。从日志输出来看,这是一个典型的点击事件的在视图层级和Activity之间的传递过程。至于为什么会这样传递,还是要回归源码来找答案。

源码分析

先从ActivitydispatchTouchEvent方法入手:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

从注释可以看出,该方法是屏幕事件的入口。如果想需要在将事件分发到window之前就拦截该事件,可以在子类中重写该方法。返回true就可以消费掉该事件,也就成功的拦截了该事件。
那么我们就试试在该方法下返回true,看看日志会怎么打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SjActivity extends Activity implements View.OnClickListener, View.OnTouchListener{
private static final String TAG = "SjActivity";
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, " >>>> SjActivity >> dispatchTouchEvent >>> DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, " >>>> SjActivity >> dispatchTouchEvent >>> MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, " >>>> SjActivity >> dispatchTouchEvent >>> UP");
break;
}
return true;
}
...
}

1
2
05-30 15:57:57.028 2310-2310/com.icedcap.viewtestdemo D/SjActivity: >>>> SjActivity >> dispatchTouchEvent >>> DOWN
05-30 15:57:57.084 2310-2310/com.icedcap.viewtestdemo D/SjActivity: >>>> SjActivity >> dispatchTouchEvent >>> UP

和注释中说的一模一样,后续视图层级不会收到事件,而视图的onClickonTouch事件也得不到回调的机会。所以在开发中如果覆写了该方法还是要注意正常返回是否要拦截事件的逻辑。

继续分析源码,首先由ACTION_DOEN事件会调用onUserInteraction()方法,该方法是一个空实现,可以由子类来覆写,并且在接收到ACTION_DOWN事件后进行相应的操作。

getWindow().superDispatchTouchEvent(ev)方法主要是进行自定义window的事件分发过程,例如Dialog

最后调用onTouchEvent方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
*
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}

从注释来看,如果视图层级中没有任何视图去操作该事件,该方法是非常有用的,主要用于不在window范围内的视图。并且它默认都是返回false

当返回false时将把事件继续往下一层进行分发。这里直接看ViewdispatchTouchEvent方法。

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

从代码中可以看到,先是进行聚焦检查,再通过事件DOWN的检查来停止当前的滑动效果,这些都不是重点。接下来onFilterTouchEventForSecurity(event)进行检查当前window是否被覆盖,若没有的话就切入重点了。首先是进行很多条件的判断,ListenerInfo类是一个包装了很多监听事件的内部类,其中就包括了OnTouchListenerOnClickListener,所以一旦为View设置了setOnTouchListener监听事件的话,li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)前三个条件都会成立,故该ViewonTouch会被回调的。若在该回调函数中返回true就不会再去调用当前的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
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
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) {
// 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();
}
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}

这里代码很长,选取了其中的一部分来分析。这里监听到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
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
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;
}

到这里就真相大白了,performClick方法就是进行onClick事件回调的入口(当然前提是设置了OnClickListener)。

在最开始的demo中,我们为每一个视图层级的和ActivitydispatchTouchEvent只进行了日志打印工作没有改变其返回的值,直接是默认的调用其父类的dispatchTouchEvent方法返回其父类的值。而在分析ActivitydispatchTouchEvent方法的时候我们在demo中返回了true后直接导致了View视图层级没有收到事件也接收不到onTouchonClick的回调操作。

这时我们在回过头来看一下ViewonTouchEvent代码,在if判断语句中,只要当前的操作是CLICKABLE点击或者LONG_CLICKABLE长按最后都会返回true继而其对应的dispatchTouchEvent就会返回true这也意味着该事件不会继续向下传递。

为了验证这个结果,我们在demo中为CustomLayoutdispatchTouchEvent返回一个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
public class CustomLayout extends FrameLayout {
private static final String TAG = "CustomLayout";
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(TAG, " >>>> dispatchTouchEvent >>>> DOWN");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, " >>>> dispatchTouchEvent >>>> UP");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, " >>>> dispatchTouchEvent >>>> MOVE");
break;
}
// return super.dispatchTouchEvent(ev);
return onTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(TAG, " >>>> onTouchEvent >>>> DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, " >>>> onTouchEvent >>>> MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, " >>>> onTouchEvent >>>> UP");
break;
}
// return super.onTouchEvent(event);
return true;
}
}

1
2
3
4
5
6
05-30 17:09:30.829 5953-5953/com.icedcap.viewtestdemo D/SjActivity: >>>> SjActivity >> dispatchTouchEvent >>> DOWN
05-30 17:09:30.829 5953-5953/com.icedcap.viewtestdemo D/CustomLayout: >>>> dispatchTouchEvent >>>> DOWN
05-30 17:09:30.829 5953-5953/com.icedcap.viewtestdemo D/CustomLayout: >>>> onTouchEvent >>>> DOWN
05-30 17:09:30.896 5953-5953/com.icedcap.viewtestdemo D/SjActivity: >>>> SjActivity >> dispatchTouchEvent >>> UP
05-30 17:09:30.896 5953-5953/com.icedcap.viewtestdemo D/CustomLayout: >>>> dispatchTouchEvent >>>> UP
05-30 17:09:30.896 5953-5953/com.icedcap.viewtestdemo D/CustomLayout: >>>> onTouchEvent >>>> UP

和分析的结果一样,如果在CustomLayoutdispatchTouchEvent方法中返回true(这也意味着在该视图层级的onTouchEvent中消费了这个事件,否则这是无意义的事情)时,事件将不会继续向下传递。

ViewGroup事件分发与处理

现在我们假设在CustomLayout类的dispatchTouchEvent方法中接收到ACTION_DOWN事件后直接返回true接收其他的动作还是继续派发。如下代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(TAG, " >>>> dispatchTouchEvent >>>> DOWN");
return true;
// break;
case MotionEvent.ACTION_UP:
Log.d(TAG, " >>>> dispatchTouchEvent >>>> UP");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, " >>>> dispatchTouchEvent >>>> MOVE");
break;
}
return super.dispatchTouchEvent(ev);
}

按照常理来说事件传递顺序应该是这样的:Activity(d : D) -> CustomLayout(d : D);Activity(d : U) -> CustomLayout(d : U) -> CustomButton(d : U) -> Activity(onTouch : U) -> CustomButton(t : U) -> Activity(onClick)

(d : D) 代表 dispatchTouchEventACTION_DOWN
(t : U) 代表onTouchEventACTION_UP

那么我们看一下日志,事件到底是怎么传递的呢?

1
2
3
4
5
6
05-30 17:29:11.636 32280-32280/com.icedcap.viewtestdemo D/SjActivity: >>>> SjActivity >> dispatchTouchEvent >>> DOWN
05-30 17:29:11.636 32280-32280/com.icedcap.viewtestdemo D/CustomLayout: >>>> dispatchTouchEvent >>>> DOWN
05-30 17:29:11.694 32280-32280/com.icedcap.viewtestdemo D/SjActivity: >>>> SjActivity >> dispatchTouchEvent >>> UP
05-30 17:29:11.694 32280-32280/com.icedcap.viewtestdemo D/CustomLayout: >>>> dispatchTouchEvent >>>> UP
05-30 17:29:11.695 32280-32280/com.icedcap.viewtestdemo D/SjActivity: CustomLayout >>>> onTouch >>>> UP
05-30 17:29:11.695 32280-32280/com.icedcap.viewtestdemo D/CustomLayout: >>>> onTouchEvent >>>> UP

ACTION_DOWN事件和想象的一样,但是对于ACTION_UP事件来说,它没有传递到下一个视图层级而是直接由CustomLayoutonTouchEvent消费掉。这又是为什么呢?看来真相还得去看源代码。ViewGroup覆写了ViewdispatchTouchEvent方法,我们就看看到底在这里搞了什么鬼。

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

当接收事件为actionMasked == MotionEvent.ACTION_DOWN时会调用cancelAndClearTouchTargetsresetTouchState分别用于清除touch目标和重置touch状态。这里维护了一个mFirstTouchTarget成员变量来用于清除touch目标和touch状态。

接下来会通过actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null两个条件判断是否拦截当前的事件,第一个条件很好理解但是对于mFirstTouchTarget != null是什么意思呢?现在暂时看不出来,那么我们继续往下看。

先略过中间这大段,直接看判断语句if (mFirstTouchTarget == null) {...} else {...}这段是将事件向子视图进行分发的逻辑,可以看到当事件由ViewGroup子元素成功处理时,mFirstTouchTarget会被赋值。这时候在回过头看,如果一旦由ViewGroup将事件拦截了mFirstTouchTarget必然不会被赋值因此就不会满足mFirstTouchTarget != null条件。当除ACTION_DOWN之外的事件传进来的时候就不会去调用onInterceptTouchEvent方法。该方法正式ViewGroup进行拦截事件的重要方法,但是从上面的分析中得出:如果在ACTION_DOWN事件中就返回true那么接下来的事件序列将不会在调用onInterceptTouchEvent方法,而是直接默认拦截该事件。所以在开发中,如果要判断滑动事件满足某一条件时进行拦截操作的话,一定不能在onInterceptTouchEvent方法的ACTION_DOWN事件的时候就返回true

ViewGroup中默认的onInterceptTouchEvent方法至返回false。拦截操作要在子类中完成。

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

上述分析的结果也正好吻合我们日志中打印的结果,我们在CustomLayoutdispatchTouchEventACTION_DOWN事件下返回了true,这就意味着要拦截当前的事件,导致mFirstTouchTarget == null,在之后的事件序列中也默认的走else { intercepted = true; }的代码块,也就是拦截了。

接下来看一看ViewGroup不拦截事件的时候,它是怎么交给子View进行处理的。首先通过遍历拿到子元素for (int i = childrenCount - 1; i >= 0; i--) {...}在循环代码块中进行一系列的判断包括子元素是否在播放动画或者点击坐标是否在子元素区域内。如果子元素满足了这两个条件,就会将事件传递给该子元素。

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
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
// 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;
}

经过以上源码+Demo的分析我们知道了View事件分发机制,接下来就上篇博客中的自定义滑动控件的bug进行修复。

实践开发

上篇博客中自定义滑动View存在这样一个bug,当手指按到中间的按钮区域的时候在进行滑动将会出现滑不动的情况。经过上述对View事件分发机制的了解,很容易就知道产生这个bug的原因。

由于没有在自定义ViewGroup进行事件拦截操作,当手指点击到按钮区域时候将会被按钮的onTouchEvent消费该事件,故在传入ACTION_UP事件的时候也会被按钮区域的View消费掉。所以我们在StretchScrollView类中完善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
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
if (!mOverScroller.isFinished()) {
mOverScroller.abortAnimation();
intercept = true;
}
break;
case MotionEvent.ACTION_MOVE:
int x = (int) ev.getX();
int y = (int) ev.getY();
int deltaX = mLastX - x;
int deltaY = mLastY - y;
intercept = Math.abs(deltaX) < Math.abs(deltaY);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
mLastX = (int) ev.getX();
mLastY = (int) ev.getY();
return intercept;
}

MotionEvent.ACTION_MOVE事件中,判断是否上下滑动,如果是就拦截该事件进行上下滑动的操作。这样就解决了手指按住按钮区域时无法滑动的bug。

总结

以上就是android中View事件分发机制的过程分析,由于代码繁多且复杂,有些方法以及逻辑还是不能全部搞懂。但是通过以上的分析对于实践开发已经足够了。

  1. Android事件分发首先由Activity开始到window的视图层级,进行一层一层的分发。
  2. 事件通过dispatchTouchEvent方法进行分发,可以通过onTouchEvent进行消费工作,对于ViewGroup来说可以通过onInterceptTouchEvent进行事件拦截操作。
  3. 如果在ViewGroupACTION_DOWN事件拦截了,那么后续的事件序列默认都将被该ViewGroup拦截并且不会再去调用onInterceptTouchEvent方法来判断是否拦截。
  4. onTouch回调要先于onTouchEvent被调用,它是在dispatchTouchEvent方法体中被调用的。而onClick事件是在最后才被调用,它的调用时机是在onTouchEvent方法体中。

参考