Androi触摸事件分发流程

事件传递是从里向外触发,也就是说是父视图先拿到事件然后依次传到子view。

ViewGroup的dispatchTouchEvent(Event e),收到一个事件,首先判断这个view是不是可见然后它有没有被遮挡。如果可见并且没有遮挡就就可以进行事件的分发。在事件分发里面有一个很重要的参数mFirstTouchTarget,这个参数只有当我们的事件找到了那个可以消费他的那个view的时候,这个mFirstTouchTarget才会被附上值。

1
2
3
//可见并且没有遮挡就就可以进行事件的分发    
if (onFilterTouchEventForSecurity(ev)) {
}

ViewGroup的dispatchTouchevent这个方法返回false就代表这个事件这个viewGroup和他的所有子view都没有一个有能力进行消费的。

当事件可以进行分发的时候,判断是不是点击事件,是的话就重置所有状态,代表这是一轮新的事件循环:

1
2
3
4
5
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 将 mFirstTouchTarget 设置为了 null
cancelAndClearTouchTargets(ev);
resetTouchState();
}

当是down事件或者mFirstTouchTarget不为null的时候,会调用viewGroup的onInterceptTouchEvent方法,判断自己这个viewGroup要不要拦截这个事件,如果拦截的话,那么这个viewGroup的所有子view都不会收到任何事件,然后被拦截的事件会交给自己这个viewGroup去处理。

1
2
3
4
5
6
 // intercepted 这是有没有拦截的标志,如果为true 代表拦截。那么里面的循环就不会执行,导致的结果就是里面的所有子view都不会收到任何事件
if (!canceled && !intercepted) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
}
}

现在问题来了,那系统怎么知道你这个事件有没有被消费呢?

解答: 我们知道ViewGroup也是继承自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
// view.java
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}
// 判断这个view是不是可见并且没有遮挡
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
// 这里可以看到 当我们给view设置了mOnTouchListener,那么result就等于true,代表这个事件,我们的View必定会去消费。
// 如果设置了mOnTouchListener 那我们的setOnClickListener事件就不会响应了。原因是onTouchEvent事件不会被触发了
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 (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}

通过上面代码可以看到。在view的dispatchTouchEvent方法里,只要这个方法返回了true,那么就代表此次事件找到了消费者。找到了那个真正会干掉它的人。

这里有一点很重要的知识点需要注意:事件有没有被消费都是在我们执行onTouchEvent或者mTouchListener的时候的返回值来决定的,如果这两个方法,我们没有返回true,那么就导致这个事件虽然传递到了我们真正点击的那个视图上面了,我们只能拿到down事件

现在来看看是怎么把事件分发到子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
for (int i = childrenCount - 1; i >= 0; i--) {
// 找到view的index
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
// 找到view
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//isTransformedTouchPointInView()用来判断点击的坐标是不是在这个view里面
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// 坐标在这个子view里面
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}

resetCancelNextUpFlag(child);
// dispatchTransformedTouchEvent 判断这个view是否成功消费了事件
// 如果成功消费了事件,newTouchTarget 会被附上值 mFirstTouchTarget 会被附上值
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 进来了就代表我们找到了那个正真消费事件的人了
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 附上值
// 给mFirstTouchTarget 附上值
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);
}

// =null 代表事件没有被消费,需要viewgroup自己去消费
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;
}
}

dispatchTransformedTouchEvent方法是用来判断这个事件有没有被成功消费,如果成功消费了,那么newTouchTarget 会被附上值 mFirstTouchTarget 会被附上值。这样就不会再走自己viewgroup的onTouchEvent事件,否则就执行自己的onTouchevent,判断需不需要消费这个事件。

这里着重讲下isTransformedTouchPointInView方法,他是怎么判断这个在屏幕按下的这一点是不是在子View内部的:

首先获取手指在父控件的相对坐标传给point数组。

然后执行transformPointToViewLocal 方法 将相对父控件的坐标转换成相对子view的坐标,中i但就是这个转换方法。

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
/**
* x y 是手指按在父控件上的时候通过event.getX() event.getY()获取的值。
* getX获得的值 是手指相对于自己的坐标
*
*/
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
public void transformPointToViewLocal(float[] point, View child) {
// 这么一减,就得出了对应在子view里面的坐标
point[0] += mScrollX - child.mLeft;
point[1] += mScrollY - child.mTop;

//hasIdentityMatrix() 鉴定这个view所所对应的矩阵有没有应用过比如setTranslation setRotation setScale 这些方法。如果有返回false 没有返回true
if (!child.hasIdentityMatrix()) {
child.getInverseMatrix().mapPoints(point);
}
}
public boolean pointInView(float localX, float localY, float slop) {
return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
localY < ((mBottom - mTop) + slop);
}
img1

现在分析transformPointToViewLocal的计算原理:

img1

能执行dispatchTransformedTouchEvent方法的对象一定是一个viewgroup,他的执行需要传一个 child:View 的参数,这个参数如果为null就代表,执行super.dispatchTouchEvent(event);也就是自己父类的dispatchTouchEvent(自己的父类也就是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
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
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
// 执行自己父类(view)的dispatchTouchEvent 也相当于去执行onTouchEvent方法
handled = super.dispatchTouchEvent(event);
} else {
// child不等于null代表去执行 自己孩子的dispatchEvent方法
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;
}

以上整个事件分发讲完。

总结:

事件最先由viewGroup进行分发,进入dispatchTouchEvent方法后,他会先掉用onInterceptTouchEvent方法判断事件是否拦截,如果拦截,也就是返回true,那么这个事件就交给自己这个viewgroup处理,执行他自己的onTouchEvent方法。如果不拦截返回false,那么进入for循环,遍历自己的全部子view,调用dispatchTransformedTouchEvent方法,依次调用子view的onTouchEvent方法,如果down事件还是没有被消费,然后就又回到自己这个viewgroup里面,执行子的onTouchEvent方法。