Alec's blog

唯纯粹与热爱不可辜负

0%

Android事件分发机制

基本知识

三个相关主体

  • Activity

  • ViewGroup

  • View

    他们三个的嵌套关系一般是这样:

但是还要明白的是:

  • ViewGroup当然可以嵌套ViewGroup,即ViewGroup也可以是另一个ViewGroup的子View。
  • ViewGroup其实是继承于View,是View的子类。

事件分发机制相关三个经典函数

  • dispatchTouchEvent():分发函数
  • onInterceptTouchEvent():拦截函数
  • onTouchEvent():消费函数

它们的功能和它们名字一样。其中拦截函数是ViewGroup独有的,其它两个函数在上面说的三个主体都存在。

事件分发机制四个经典事件

  • ACTION_DOWN
  • ACTION_MOVE
  • ACTION_UP
  • ACTION_CANCLE

意如其名

事件分发机制场景

参考blog

不拦截、不消费

当三个主体的任何函数的 返回值 都不做任何处理时,即不拦截、不消费:


可见:

  • 对于down事件:我会从外层一层层地分发下去(Activity->ViewGroup->view),看看
  • down不消费,move,up我就不传递去了

ViewGroup拦截、无消费

当在ViewGroup使onInterceptTouchEvent()返回true,即ViewGroup对事件进行拦截时:


可见:

  • 事件被拦截之后就不会往下分发

ViewGoup消费,不拦截

当在ViewGroup使onTouchEvent()返回true,即ViewGroup对事件进行消费时:

可见:

  • 当down被消费了就不会往上冒
  • move up不会往下发,而是直接分发给消费者。

源码分析

具体代码怎么实现?主要是看分发函数dispatchTouchEvent(),接下来我们看看 三个主体的dispatchTouchEvent() 源码分析

Activity的dispatchTouchEvent()源码

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();//这是一个空方法
}
//主要看这一句
//getWindow().superDispatchTouchEvent(ev)
//这句函数调用的时DecorView的superDispatchTouchEvent()
//而DecorView继承于ViewGroup
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

代码中我写了注释,我们可以得出下面的结论:

  • getWindow().superDispatchTouchEvent(ev),实际是调用了一个ViewGroup的dispatchTouchEvent()
  • getWindow().superDispatchTouchEvent(ev)返回true,说明有子View消费该事件(为什么呢?我们要分析完ViewGroup的dispatchTouchEvent()才知道,但现在可以暂时给出这个结论);这个子View可能是某个ViewGroup或者View。
  • 如果有子view消费该事件则返回true,否则调用自身的onTouchEvent(ev),即把事件分发给自己。

ViewGroup的dispatchTouchEvent()源码

ViewGroup的dispatchTouchEvent()源码很长,我参考了https://blog.csdn.net/wolinxuebin/article/details/53057075
之后得出一些小结,现在贴出一部分,一段一段分析。

总体逻辑分析

我把部分代码和具体逻辑去掉,看它的空架子

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
//这是一个单链表,我暂时理解为用于存放响应了DOWN的事件
private TouchTarget mFirstTouchTarget;

public boolean dispatchTouchEvent(MotionEvent ev) {
...
//判断是否是模糊窗口,如果是窗口,则表示不希望处理改事件。(如dialog后的窗口)
if (onFilterTouchEventForSecurity(ev)) {
// 清空之前的状态
if (actionMasked == MotionEvent.ACTION_DOWN) {}

//检查是否需要拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {} else {}

//检查是否要取消,即标记了PFLAG_CANCEL_NEXT_UP_EVENT 或者 当前是一个Cancel事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;

//不用取消,无需拦截,则进行事件分发
if (!canceled && !intercepted) {
//分发DOWN事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//分发DOWN给child
if (newTouchTarget == null && childrenCount != 0) {

}

//如果没有child相应该事件,则将此事件交给最近加入的target?
//这里不是很懂,如果没有child响应,那么mFirstTouchTarget也是null呀
if (newTouchTarget == null && mFirstTouchTarget != null) {

}
}
}

//mFirstTouchTarget为空表明没有child响应这个事件,则分发给自己
if (mFirstTouchTarget == null) {}
//按照mFirstTouchTarget分发
else {}

}
...
//如果自身或者child消费了事件则返回true,否则返回false
return handled;
}

关于解析看代码中的注释。接下来看看其中几段逻辑具体怎么实现的。

具体分析一

看一下分发给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
//分发DOWN给child
if (newTouchTarget == null && childrenCount != 0) {

// 对子Views进行排序,有两种方式:1、按照Z轴,2、按照draw
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;

//遍历子View
for (int i = childrenCount - 1; i >= 0; i--) {
//这里两行代码,简单的理解根据不同的排列选项(1、view添加到 2、view的draw顺序 3、viewZ 轴顺序)
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);

//canViewReceivePointerEvents 判断child是否为visiable 或者 是否有动画
//isTransformedTouchPointInView 判断x, y是否在view的区域内(如果是执行了补间动画 则x,y会通过获取的matrix变换值
// 换算当相应的区域,这也是为什么补间动画的触发区域不随着动画而改变)
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}

//如果chile已经在mFirstTouchTarget单链表里面,结束循环
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;
}

//判断child的dispatchTouchEvent()是否会返回true,如果是true,将child加入单链表,然后结束循环
//dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)会调用child的dispatchTouchEvent()
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// 找到childIndex所代表的child的最原始的index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}

//将相应该事件的child包装成一个Target,添加到mFirstTouchTarget链表中
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}

这里我只贴出部分的代码,解析都在注释,可以看到,mFirstTouchTarget链表只存在一个值,就是响应了事件的那个child。

具体分析二

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
//这一段的要么分发down给自己要么按照单链表分发move、up
//伪代码
//
if (mFirstTouchTarget == null) {
//没有child响应事件,则分发给自己,handled将作为返回值返回。
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;
//在前面Down事件处理中,已经将这个事件交给newTouchTarget处理过了,就不重复处理了
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//这里其实是分发move和up事件,因为down事件在前面已经处理完了,不会进入这里
//再次判定是否需要cancel,因为有可能在move过程事件被拦截
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//如果cancel,回收链表节点空间,最后使mFirstTouchTarget置null
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}

View的dispatchTouchEvent()源码

同样的,我省略了部分代码,解析看注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;

if (onFilterTouchEventForSecurity(event)) {
//如果设置了onTouchListener,会先调用onTouch()
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

//调用onTouchEvent()
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}

可以看到:

  • View的dispatchTouchEvent()的返回值取决于onTouch()或onTouchEvent(),也就是有没有消费该事件。
  • 如果onTouch()返回true的话就不会调用onTouchEvent(),这就是为什么有些博客写onTouch()优先于onTouchEvent()。

总结

分析源码就可以知道前面说的三种场景是怎么回事了。

有点像皇帝派任务,皇帝说现在有个好活儿,但是不知道谁要接这个活儿,于是派了一个小太监去探测一下;
小太监先去找宰相,宰相又让他去找知府,知府让他去找衙门小兵。

这里面皇帝就像Acticity、各级官员就像ViewGroup、小兵就像View、而小太监就像DOWN事件,活儿就是跟着DOWN后面的MOVE和UP事件。

  • 不拦截、不消费:小太监一层层找到小兵后,没有一个小兵想接这个活儿(一层层分发DOWN事件),于是小兵沿路返回报告给知府、知府也不想做就报告给宰相,宰相不想做就回去报告给皇帝,皇帝说没人做那我看看自己能不能做吧(DOWN事件回到Activity派给自己,MOVE、UP也不再分发而是直接派给自己)
  • ViewGroup拦截、不消费:宰相让小太监找到知府的时候, 这个知府有点霸道直接把小太监拦下了,小太监就就不再继续通知下级人员了(拦截事件)。但是呢这个官员只是单纯拦下了小太监但他并不想接这个活儿,于是小兵还是沿路回去报告给说下面没人接活,最终还是传回给皇帝说没人做那我看看自己能不能做(MOVE、UP不再分发直接派给自己)
  • ViewGroup消费、不拦截:同样,无人拦截的话,小太监一层层找到小兵,发现小兵没人想做,就回去报告知府,这时候知府说小兵不做我来做(DOWM在这里被消费了)。然后知府写信报告宰相说这活儿我接了(返回true),宰相又报告皇帝说下面有人接受任务了,然会皇帝下次就直接派发任务给宰相,宰相找到那个愿意接受任务知府,把任务派给他。(派发MOVE、UP)

分发函数分发DOWN时其实有点类似于递归的方式,只不过不是自己调用自己,而是一层层地调用child的同名函数。分发MOVE、UP则不再一一询问,而是根据DOWN是否被消费进行分发。

源码很长现在头都有点乱,那么从源码学习到了啥。

  • 如有有这种嵌套式的应答需求,可以学习ViewGroup的分发函数,用类似于递归的方式提问和接收应答。
  • 对于较大量的信息,命令传送,可以先派一个小兵嗅探一下,记录可行的路线,后续信息按路线分发。