事件分发
1.在Android
体系中,事件分发机制占有重要的一份,了解事件的分发机制,对于滑动等冲突才有更深刻的理解。自定义View
中能更好的扩展,遇到相关问题能从整个流程上思考,寻找最优解决办法。先上图:
从Activity开始
(图片来自网络)
1.通过上图,Activity
的层级关系清晰明了。Activity
在创建之初,通过addView
操作,将View
一层层的”贴附上”。直观上最上层View
则是被最后添加,也正是因为这个特点,当事件传递过程中源码对于子View
采用了倒序遍历,增大命中机率。这个后面会具体分析。了解View
的层级有利于对事件传递更好的理解。
2.无论点击事件,滑动事件,总是会包含几个状态。ACTION_DOWN---ACTION_UP、ACTION_DOWN-MOVE-MOVE-...-ACTION_UP
。既然用户的操作首先作用到Activity
上,那么首先从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
41Activity中的dispatchTouchEvent();
/**
* 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);
}
public void onUserInteraction() {
}
/**
* 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;
}
3.可以看到的是onTouchEvent默认实现是false,注释里解释的也很清楚,事件到此结束。但是有个前提的是getWindow().superDispatchTouchEvent(ev) = false
。而getWindow返回的是window,window作为接口,它的唯一实现PhoneWindow
,superDispatchTouchEvent(ev)
调用了父类的方法也即ViewGroup.dispatchTouchEvent
:1
2
3
4
5
6
7PhoneWindow
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
window的作用更像是一个工人,起到了连接的作用,这里的mDecor = DecorView,DecorView继承自FrameLayout,FrameLayout继承自Viewgroup
mDecor.superDispatchTouchEvent(event),最终调用的是Viewgroup中的dispatchTouchEvent方法。
4.总结一下,当事件被activity接收,并可以向下传递,则传递的顺序为activity.dispatchTouchEvent--->PhoneWindow.superDispatchTouchEvent(ev)--->DecorView.superDispatchTouchEvent(event)--->ViewGroup.dispatchTouchEvent
。事件由此传递到ViewGroup,重点分析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
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...省略...
public boolean dispatchTouchEvent(MotionEvent ev) {
//当事件传入到viewgroup的dispatchTouchEvent方法时,因为一个完整的事件序列,以ACTION_DOWN开始。这里对ACTION_DOWN首先判断
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//判断viewgroup是否需要拦截此次事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}
...省略...
}
//viewgroup默认是不拦截事件 return false
public boolean onInterceptTouchEvent(MotionEvent ev) {
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;
}
//倒序遍历子view,对view作了判断,是否可见,是否在执行动画,点击范围是否落在view上;从而决定view是否消费此次事件
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 = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, 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 (!child.canReceivePointerEvents()
|| !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();
}
//view没有拦截事件方法,默认来处理事件,如果可以的话。dispatchTouchEvent事件同样可以认为是将事件分发给自己处理。
//view中的dispatchtouchevent方法
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
//包含了,长按,点击,ontouch等监听。
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
//这里可以发现,如果view设置了mOnTouchListener监听,它的优先级是很高的,在ontouchevent之前。
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
}
public boolean onTouchEvent(MotionEvent event) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
case MotionEvent.ACTION_DOWN:
if (!clickable) {
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
}
/**
* Defines the default duration in milliseconds before a press turns into
* a long press
*/
private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500;
//在view的ontouchevent事件中,首先要对长按事件作处理,
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
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
89private void checkForLongClick(long delay, float x, float y, int classification) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
mPendingCheckForLongPress.setClassification(classification);
postDelayed(mPendingCheckForLongPress, delay);
}
}
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().postDelayed(action, delayMillis);
return true;
}
private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;
private boolean mOriginalPressedState;
/**
* The classification of the long click being checked: one of the
* StatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__* constants.
*/
private int mClassification;
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
recordGestureClassification(mClassification);
//
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
....
}
public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
final boolean handled = performLongClick();
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}
//在ACTION_DOWN中,对长按的处理,处理逻辑很简单:通过post发送了一个500ms的消息,如果500ms内事件得以消费,返回true则长按事件被处理,否咋将会在ACTION_UP与ACTION_CANCEL中将事件移除-->removeLongPressCallback()对与click事件则是在ACTION_UP作出处理:
//view可点击,并且接收到down与up事件,因为click事件是在up中被处理
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)) {
performClickInternal();
}
}
}
对于view中还有一段
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
这里解释了一个即使view是disable的状态依然可以消费事件,只是不对事件作出反应。
总结
1.对于一个ViewGroup,点击事件产生后,首先会传递给它,这时他的dispatchTouchEvent会调用,如果它的onInterceptTouchEvent返回true表示要拦截当前事件,接下来事件会交给这个ViewGroup处理,它的onTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent返回false,则事件会继续传递给子元素,子元素的dispatchTouchEvent会调用,如此反复直到事件被处理完毕。
2.当一个View需要处理事件时,如果设置了OnTouchListener,那么OnTouchListener的onTouch方法会回调,如果onTouch返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不会调用。由此可见,OnTouchListener优先级高于onTouchEvent。OnClickListener优先级处在事件传递的尾端。
3.一个点击事件产生后,传递顺序:Activity->PhoneWindow->View;如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent会被调用,以此类推,所有元素都不处理该事件,最终将传递给Activity处理,即Activity的onTouchEvent会被调用。
4.通常一个事件序列只能被一个View拦截且消耗,同一个事件序列所有事件都会直接交给它处理,并且它的onInterceptTouchEvent不会再被调用。如果子view中调用requestDisallowInterceptTouchEvent,则会决定父view是否拦截事件(除action_down以外的事件,action_down会重置FLAG_DISALLOW_INTERCEPT的状态值)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
}
5.某个View一旦开始处理事件,如果它不消耗ACTION_DOWN(onTouchEvent返回了false),那么同一事件序列中其他事件都不会再交给它来处理,事件将重新交给他的父元素处理,即父元素的onTouchEvent会被调用。
6.如果某个View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以收到后续事件,最终这些消失的点击事件会传递给Activity处理。
7.ViewGroup默认不拦截任何事件,ViewGroup的onInterceptTouchEvent方法默认返回false,View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。
8.View的onTouchEvent方法默认消耗事件(返回true),除非他是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性分情况,Button默认为true,TextView默认为false。disable不会影响事件的消费,即时一个view是disable状态,依然会消费事件,只是用户无感知,即无反馈。