View的事件分发机制解析

引言

Android事件构成

在Android中,事件主要包括点按、长按、拖拽、滑动等,点按又包括单击和双击,另外还包括单指操作和多指操作。所有这些都构成了Android中的事件响应。总的来说,所有的事件都由如下三个部分作为基础:

  • 按下(ACTION_DOWN)
  • 移动(ACTION_MOVE)
  • 抬起(ACTION_UP) 所有的操作事件首先必须执行的是按下操作(ACTION_DOWN),之后所有的操作都是以按下操作作为前提,当按下操作完成后,接下来可能是一段移动(ACTION_MOVE)然后抬起(ACTION_UP),或者是按下操作执行完成后没有移动就直接抬起。这一系列的动作在Android中都可以进行控制。

这些操作事件都发生在我们手机的触摸屏上面,而我们手机上响应我们各种操作事件的就是各种各样的视图组件也就是View,在Android中,所有的视图都继承于View,另外通过各种布局组件(ViewGroup)来对View进行布局,ViewGroup也继承于View。所有的UI控件例如Button、TextView都是继承于View,而所有的布局控件例如RelativeLayout、容器控件例如ListView都是继承于ViewGroup。所以,我们的事件操作主要就是发生在ViewViewGroup之间。

事件分发的概念

所谓点击事件的事件分发,就是当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View(ViewGroup也继承于View),这个传递的过程就叫做分发过程,这个点击事件的分发过程需要三个很重要的方法来共同完成:disPatchTouchEvent、onInterceptTouchEvent、onTouchEvent

  • public boolean disPatchTouchEvent(MotionEvent ev) 用来进行事件的分发,如果事件能够传递给当前的View,那么此方法一定会被调用,Android中所有的点击事件都必须经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。返回true表示不继续分发,事件已被消费,返回false则继续往下分发,如果是ViewGroup则分发给onInterceptTouchEvent进行判断是否拦截该事件,这个方法的返回结果受到当前ViewonTouchEvent和下级ViewdisPatchTouchEvent方法的影响,返回结果表示是否消耗当前事件。

  • public boolean onTouchEvent(MotionEvent ev)diaPatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列当中,当前View无法再次接收到事件。

  • public boolean onInterceptTouchEvent(MotionEvent ev) 是ViewGroup中才有的方法,View中没有,它的作用是负责事件的拦截,返回true的时候表示拦截当前事件,不继续往下分发,交给自身的onTouchEvent进行处理。返回false则不拦截,继续往下传。这是ViewGroup特有的方法,因为ViewGroup中可能还有子View,而在Android中View中是不能再包含子View的(IOS可以),在上述方法内部被调用,如果当前View拦截了某个事件,那么同一个事件序列中(指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,中间含有不定的ACTION_MOVE事件,最终以ACTION_UP事件结束)此方法不会被再次调用,返回结果表示是否拦截当前事件。

这三个方法可以用如下伪代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
public boolean disPathchTouchEvent(MotionEvent ev){
//consume指代点击事件是否被消耗
boolean consume=false;
//表示当前父布局要拦截该事件
if(onInterceptTouchEvent(MotionEvent ev)){
consume=onTouchEvent(ev);
}else{
//传递给子元素去处理
child.disPatchTouchEvent(ev);
}
return consume;
}

分析View的事件分发机制 为了简单起见我们先从View的事件分发机制开始分析,然后在分析ViewGroup的,首先我们建一个简单的项目,这个项目里只有一个Button,并且我们给这个Button设置点击事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="100dp"
android:text="Click me" />

</RelativeLayout>
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
package com.example.testbtn;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.widget.Button;

public class MainActivity extends Activity implements OnClickListener,OnTouchListener{

private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn=(Button) findViewById(R.id.btn);
btn.setOnClickListener(this);
btn.setOnTouchListener(this);
}
@Override
public void onClick(View v) {
Log.d("TAG", "OnClick");
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch"+event.getAction());
return false;
}


}

界面是这样的:

这里写图片描述

运行这个程序,点击Button,查看Log打印输出的信息:

这里写图片描述

(这里onTouch0代表的是ACTION_DOWN,onTouch1代表的是ACTION_UP,onTouch2表示ACTION_MOVE,因为我们只是稳稳的点击了一下Button所以不会有ACTION_MOVE的Log信息出现) 这样我们可以得到一个初步的结论:onTouch()方法是优先于onClick()执行的,然后我们会发现onTouch()方法有一个很明显的和onClick()方法不同的地方的,那就是它有一个Boolean类型的返回值,如果我们把这个默认为False的返回值改为True会怎么样呢:

这里写图片描述

发现了什么:onClick()方法没有被执行,这里我们把这种现象叫做点击事件被onTouch()消费掉了,事件不会在继续向onClick()方法传递了,那么事件分发机制最基本的几条我们已经了解了,下面我们来分析产生这种机制的根本原因。

View对点击事件的处理过程

首先我们给出一个结论:Android中所有的事件都必须经过disPatchTouchEvent(MotionEvent ev)这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理,那么我们就来看看这个disPatchTouchEvent(MotionEvent ev)到底干了什么。

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

代码有点多,我们一步步来看:

1
2
3
4
5
6
7
8
9
// 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);
}

最前面这一段就是判断当前事件是否能获得焦点,如果不能获得焦点或者不存在一个View那我们就直接返回False跳出循环,接下来:

1
2
3
4
5
6
7
8
9
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}

设置一些标记和处理input与手势等传递,不用管,到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 (onFilterTouchEventForSecurity(event))是用来判断View是否被遮住等,ListenerInfoView的静态内部类,专门用来定义一些XXXListener等方法的,到了重点:

1
2
3
4
5
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

很长的一个判断,一个个来解释:第一个li肯定不为空,因为在这个If判断语句之前就new了一个li,第二个条件li.mOnTouchListener != null,怎么确定这个mOnTouchListener不为空呢?我们在View类里面发现了如下方法:

1
2
3
4
5
6
7
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}

意味着只要给控件注册了onTouch事件这个mOnTouchListener就一定会被赋值,接下来(mViewFlags & ENABLED_MASK) == ENABLED是通过位与运算来判断这个View是否是ENABLED的,我们默认控件都是ENABLED的所以这一条也成立,最后一条li.mOnTouchListener.onTouch(this, event)是判断onTouch()的返回值是否为True,我们后面把默认为False的返回值改成了True,所以这一整系列的判断都是True,那么这个disPatchTouchEvent(MotionEvent ev)方法直接就返回了True,那么接下来的代码都不会被执行,我们下面有这么一段代码:

1
2
3
if (!result && onTouchEvent(event)) {
result = true;
}

最开始我们onTouch()方法的返回值是False的,那么

1
2
3
4
5
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

这里面的判断就不成立,result最开始的默认值也是false,那么此时如果

1
onTouchEvent(event)

返回值也是True,那么if (!result && onTouchEvent(event))这个方法判断条件成立,disPatchTouchEvent(MotionEvent ev)返回True,否则返回False。

这里我们得到两个结论:

  • OnTouchListener的优先级比onTouchEvent要高,联想到刚才的小Demo也可以得出onTouch方法优先于onClick()方法执行(onClick()是在onTouchEvent(event)方法中被执行的这个待会会说到)
  • 如果控件(View)的onTouch返回False或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是ENABLE的情况下会调用onTouchEvent方法,此时dispatchTouchEvent方法的返回值与onTouchEvent的返回值一样。

那么接下来我们就分析dispatchTouchEvent方法里面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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
* 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) {
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;
}

代码还是很多,我们依然一段一段来分析,最前面的一段代码:

1
2
3
4
5
6
7
8
9
10
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);
}

根据前面的分析我们知道这一段代码是对当前View处于不可用状态的情况下的分析,通过注释我们知道即使是一个不可用状态下的View依然会消耗点击事件,只是不会对这个点击事件作出响应罢了,另外通过观察这个return返回值,只要这个ViewCLICKABLELONG_CLICKABLE或者CONTEXT_CLICKABLE有一个为True,那么返回值就是True,onTouchEvent方法会消耗当前事件。

看下一段代码:

1
2
3
4
5
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

这段代码的意思是如果View设置有代理,那么还会执行TouchDelegateonTouchEvent(event)方法,这个onTouchEvent(event)的工作机制看起来和OnTouchListener类似,这里不深入研究。 –《Android开发艺术探索》

下面看一下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
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;
}

我们还是一行行来分解:

1
2
3
4
5
6
7
8
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
//省略
}
return false;
}

这个判断之前描述过不再赘述,如果这个判断不成立直接跳到方法尾部返回False,如果判断成立则继续进入方法内部进行一个switch(event)的判断,这里ACTION_DOWN和ACTION_MOVE都只是进行一些必要的设置与置位,我们主要看ACTION_UP:

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

首先判断了是否被按下 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;接下来判断是不是可以获得焦点,同时尝试去获取焦点:

1
2
3
4
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}

经过种种判断后我们看到这一行:

1
2
3
if (!post(mPerformClick)) {
performClick();
}

这是判断如果不是longPressed则通过post在UI Thread中执行一个PerformClick的Runnable,也就是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;
}

我们发现了什么,那就是当ACTION_UP事件发生时,会触发performClick()方法,如果这个View设置了OnClickListener那么最终会执行到OnClickListener的回调方法onClick(),这也就验证了刚才所说的:onClick()方法是在onTouchEvent内部被调用的。 同我们前面找到mOnTouchListener在哪里赋值的一样,我们也可以找到mOnClickListener在哪里赋值的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
*
* @param l The callback that will run
*
* @see #setClickable(boolean)
*/
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}

我们知道ViewLONG_CLICKABLE属性默认是False的,需要的话我们可以自己在xml或者java文件中去设置,但是CLICKABLE的False与True是和具体的View有关的,比如我们知道Button是可点击的,但是TextView默认是不可点击的,但是如果给TextView设置了点击事件,那么根据

1
2
3
if (!isClickable()) {
setClickable(true);
}

这几行代码TextView也会被设置为可点击的,同理还有setOnLongClickListener也有这种作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Register a callback to be invoked when this view is clicked and held. If this view is not
* long clickable, it becomes long clickable.
*
* @param l The callback that will run
*
* @see #setLongClickable(boolean)
*/
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}

总结

到此,View的事件分发机制已经分析完了,整个过程查了许多资料,最主要的是跟着任玉刚老师的《Android开发艺术探索》学习,最后把这个学习的过程记录下来就是这篇博客了,等有时间的时候把ViewGroup的事件分发机制也分析一遍。

主要参考 Android触摸屏事件派发机制详解与源码分析一(View篇) Android事件传递机制

请看后续 ViewGroup的事件分发机制

文章作者: 李牧羊
文章链接: https://www.limuyang.cc/2016/04/03/View的事件分发机制解析/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Atlantis