Android中触摸事件分发机制(3):ViewGroup篇

在上一篇博客中,我们讲解了View的事件分发过程,这稿博客将分析ViewGroup的事件分发过程,并且在最后给出相应的流程图。

在”Android触摸事件分发机制(一):Activity篇”的最后,我们说到ViewGroup的触摸事件分发是从dispatchTouchEvent()方法开始的,下面我们就看一下这个方法:

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
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
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
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;
}
// 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 (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 (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
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;
}
}
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;
}

由于ViewGroup继承自View,而View中也有dispatchTouchEvent()方法,所以ViewGroup中的dispatchTouchEvent()是覆写了View中的dispatchTouchEvent(),也说明了它们对触摸事件的处理有所不同。

为了详细地了解整个触摸事件传递过程,这里从ACTION_DOWN开始进行分析。

首先是onFilterTouchEventForSecurity(),这个在View的事件分发中已经讲过,就不再赘述;还有由于MotionEvent.ACTION_DOWN是触摸事件的开始,所以如果是ACTION_DOWN事件的话就需要重置状态。开始时是ACTION_DOWN状态,所以进入cancelAndClearTouchTargets()中,下面是该方法:

cancelAndClearTouchTargets()
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
/**
* Cancels and clears all touch targets.
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}

resetCancelNextUpFlag()和dispatchTransformedTouchEvent()到后面再讲,这里暂时不影响我们理解,就先看clearTouchTargets();该方法如下:

clearTouchTargets()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Clears all touch targets.
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}

显然,会将所有的TouchTarget都回收,并且使mFirstTouchTarget为null.

回到dispatchTouchEvent()方法中,虽然此时mFirstTouchTarget为null,但是actionMasked==MotionEvent.ACTION_DOWN成立,所以会进入到第一个分支中。
下面首先看mGroupFlags的值。我们看一下它在哪里被赋值,找了一下,发现是在requestDisallowInterceptTouchEvent()方法中,如下所示:

requestDisallowInterceptTouchEvent()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}

显然,如果disallowIntercept为true的话,mGroupFlags的相应位就与FLAG_DISALLOW_INTERCEPT一致,否则与FLAG_DISALLOW_INTERCEPT相反。假设在ViewGroup的外部调用了requestDisallowInterceptTouchEvent(true);则这里intercepted始终为false,否则进入到onInterceptTouchEvent()中,该方法如下:

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

这个方法异常的简单,即默认情况下返回false,如果用户重写了该方法,比如返回true,则ViewGroup会截取之后的事件,在ViewGroup中的子View就接收不到后面的事件了。

还有一个分支就是如果不是ACTION_DOWN事件,并且mFirstTouchTarget==null,则表示没有相应的事件接收目标,自然也就没必要再往下传递事件了,所以此时intercept=true;

接着往下看,是确认事件是否被取消,如果是的话就不会进行后续的处理。

然后是获取一个boolean变量split来标记是否把事件分发给多个子View,它的赋值其实跟上面的requestDisallowInterceptTouchEvent()类似,是在setMotionEventSplittingEnabled()中,该方法如下所示:

setMotionEventSplittingEnabled()
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
/**
* Enable or disable the splitting of MotionEvents to multiple children during touch event
* dispatch. This behavior is enabled by default for applications that target an
* SDK version of {@link Build.VERSION_CODES#HONEYCOMB} or newer.
*
* <p>When this option is enabled MotionEvents may be split and dispatched to different child
* views depending on where each pointer initially went down. This allows for user interactions
* such as scrolling two panes of content independently, chording of buttons, and performing
* independent gestures on different pieces of content.
*
* @param split <code>true</code> to allow MotionEvents to be split and dispatched to multiple
* child views. <code>false</code> to only allow one child view to be the target of
* any MotionEvent received by this ViewGroup.
* @attr ref android.R.styleable#ViewGroup_splitMotionEvents
*/
public void setMotionEventSplittingEnabled(boolean split) {
// TODO Applications really shouldn't change this setting mid-touch event,
// but perhaps this should handle that case and send ACTION_CANCELs to any child views
// with gestures in progress when this is changed.
if (split) {
mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;
} else {
mGroupFlags &= ~FLAG_SPLIT_MOTION_EVENTS;
}
}

如果事件既没有被取消,也没有被当前ViewGroup截取,那就会进入下面复杂的处理了。

由于现在是ACTION_DOWN事件,那么actionIndex为0,后面那些idBitsToAssign和removePointersFromTouchTargets()不影响我们理解,暂时先不讲。

现在newTouchTarget为null,如果ViewGroup中有子View的话,则进入该分支中。

首先是获取到x和y坐标,然后是通过前序遍历的方法来构建所有的子View,之所以采用前序遍历,是因为preorderedList中的顺序是按照addView或者XML布局文件中的顺序而来的,后addView添加的子View,会因为Android的UI后刷新机制显示在上层。假如点击的地方有两个子View都包含在其中,那么后添加进去的那个子View会先响应事件,这样其实也是符合人的思维方式的,因为后被添加的子View会在图层的上层,自然我们点击时也是希望上层的View最先响应。

结合上面的分析,就能理解for循环采用倒序遍历的原因了。之后通过canViewReceivePointerEvents()和isTransformedTouchPointInVieww()方法来判断当前child是否能够接受事件,并且点击位置是否在child的范围内,如果不满足则continue.

一直遍历到找到第一个符合上述条件的child为止,找到之后利用进入getTouchTarget()方法中,如下所示:

getTouchTarget()
1
2
3
4
5
6
7
8
9
10
11
12
/**
* Gets the touch target for specified child view.
* Returns null if not found.
*/
private TouchTarget getTouchTarget(View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}

显然,getTouchTarget()是遍历以mFirstTouchTarget为首的TouchTarget链条,看child是否存在某个TouchTarget中。

前面分析过,此时为ACTION_DOWN事件,所以mFirstTouchTarget为null,从而getTouchTarget()返回null.所以暂时不会break,而是执行到dispatchTransformedTouchEvent()处,这个方法非常重要。下面是该方法体:

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
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
/**
* 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;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
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);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}

首先判断是否要取消事件,如果要取消的话则进行判断,如果child为null则执行super.dispatchTouchEvent(),即当前ViewGroup作为View的dispatchTouchEvent()方法,关于View的dispatchTouchEvent()方法,在上一篇博客中详细讲过,此处不再赘述;如果child不为null,则调用child.dispatchTouchEvent(),注意这里其实就进入递归了,因为dispatchTransformedTouchEvent()是在ViewGroup#dispatchTouchEvent()中被调用,而这里又调用child.dispatchTouchEvent()方法,如果child也是ViewGroup,则进入ViewGroup的dispatchTouchEvent(),否则调用的是View的dispatchTouchEvent()方法。

如果不取消事件或者不是ACTION_CANCEL事件,则分两种情况,一种是newPointerIdBits==oldPointerIdBits,此时说明当前的事件和之前的事件相同,因而不需要变形,可直接调用super.dispatchTouchEvent(event)或者child.dispatchTouchEvent(event),否则需要调用MotionEvent.obtain(event)获得transformedEvent,之后对变形后的事件进行类似的处理。

如果事件被child或其子View消费掉,则dispatchTransformedTouchEvent()会返回true,从而进入下面的分支:先获取到mLastTouchDownTime,mLastTouchDownIndex和mLastTouchDownX,mLastTouchDownY,之后通过addTouchTarget()得到一个TouchTarget对象,下面是该方法:

addTouchTarget()
1
2
3
4
5
6
7
8
9
10
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}

之后跳出循环,后面的处理其实主要是针对非ACTION_DOWN的情况,可以看到后续还有dispatchTransformedTouchEvent()方法的调用,即为非ACTION_DOWN情况下也进行类似的递归调用,只要有一个child消费了该事件,就会使handled=true;从而返回true.

非ACTION_DOWN情形要简单得多,主要就是160行之后的处理,上面已经分析过,就不再赘述了。

至此,我们可以得到ViewGroup的触摸事件分发机制的流程图:

ViewGroup_dispatchTouchEvent

实例

在工作中遇到的两个触摸事件冲突的问题,我觉得是很好的实例。

1)RecyclerView中第一个item是ViewPager,在下拉这个item时,会将下拉变成点击ViewPager中的点击事件,从而进入电影详情页。

显然这是触摸事件冲突导致的,本来应该是RecyclerView的下拉事件,由于被ViewPager消费,从而变成点击事件,那知道了原因,解决的方法也就很简单了。

首先定义一个手势识别对象:

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
private void initGestureDetector() {
gestureDetector = new GestureDetector(null, new GestureDetector.OnGestureListener() {
@Override public boolean onDown(MotionEvent e) {
return false;
}
@Override public void onShowPress(MotionEvent e) {
}
@Override public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return Math.abs(distanceY) > Math.abs(distanceX);
}
@Override public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
});
}

即如果发现Y反向的移动量大于X方向的移动量,就返回true.

之后,在AutoScrollViewPager的dispatchTouchEvent中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override public boolean dispatchTouchEvent(MotionEvent ev) {
int action = MotionEventCompat.getActionMasked(ev);
...
if (null == gestureDetector) {
initGestureDetector();
}
if (gestureDetector.onTouchEvent(ev)) {
getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
...
getParent().requestDisallowInterceptTouchEvent(true);
return super.dispatchTouchEvent(ev);
}

上面将无关的代码省略了,可见发现Y方向的移动量大于X方向的移动量时,就调用getParent().requestDisallowInterceptTouchEvent(false);让parent(即RecyclerView)截获事件。

2)图片查看器,单击图片放大,双击退出

类似的,需要一个GestureDetector,其初始化如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
mGestureDetector = new GestureDetector(imageView.getContext(),
new GestureDetector.SimpleOnGestureListener() {
// forward long click listener
@Override
public void onLongPress(MotionEvent e) {
if (null != mLongClickListener) {
mLongClickListener.onLongClick(getImageView());
}
}
});
mGestureDetector.setOnDoubleTapListener(this);

而onDoubleTap方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public final boolean onDoubleTap(MotionEvent ev) {
try {
float scale = getScale();
float x = ev.getX();
float y = ev.getY();
if (scale < mMidScale) {
setScale(mMidScale, x, y, true);
} else if (scale >= mMidScale && scale < mMaxScale) {
setScale(mMaxScale, x, y, true);
} else {
setScale(mMinScale, x, y, true);
}
} catch (ArrayIndexOutOfBoundsException e) {
// Can sometimes happen when getX() and getY() is called
}
return true;
}

即双击时放大。

那单击退出是如何实现的呢?还记得我们在前面分析过,触摸事件是从Activity开始分发的吗!那就简单了,重写Activity中的dispatchTouchEvent()方法,在里面进行手势判断,但是需要注意不要消费事件,否则PhotoViewAttacher接收不到触摸事件!

故代码如下: