天花板谈Android开发安卓开发安卓Android基础开发

浅析安卓事件分发机制源码

2017-07-16  本文已影响114人  半岛铁盒里的猫

原文为我在csdn发表的博文:
http://blog.csdn.net/sinat_23092639/article/details/74858558

最近工作需要需要做一些比较复杂的自定义 View,其中事件分发的处理自然少不了,
结合之前阅读过的大量资料,工作是完成了,但是对事件分发的处理总觉得很不清晰,
知其然不知其所以然的感觉让人很不舒服。如果不知道事件分发原理,要是处理的情况
很复杂的话,那就很难解决了。之前也看过任玉刚的《安卓开发艺术探索》对于事件分
发源码的分析,但只能说大致了解了事件分发的流程,而不知其中的道理。网上关于事件分发的文章也是非常多,但是总感觉没说到原理,即是说了也很让人雾里看花。

索性践行某位大师的名言——
“read the fucking source”
源码的阅读总是让人痛并快乐着,一是因为源码很长逻辑很复杂,二是因为源码要考虑
的东西太多,所以干扰的东西实在太多,经常跟进去就迷路。
介于本文是对源码层次的分析,所以如果大家还没了解过事件分发的基本流程的话,最
好先看一下这方面的资料。
首先,关于事件分发,相信接触过的人都看过类似这样一张 U 型图片:

image.png

并且也知道 dispatchTouchEvent、InterceptTouchEvent、onTouchEvent 这三个方
法的作用分别分发事件、拦截事件、消费事件。
也看过类似的伪代码:

public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默认状态为没有消费过
if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子 View
result = child.dispatchTouchEvent(ev);
}
if (!result) {
// 如果事件没有被消费,询问自身 onTouchEvent
result = onTouchEvent(ev);
}
return result;
}

其实如果熟悉这些,简单的事件分发已经可以处理了,但是这些只是单纯记忆流程,并
不知道具体的事件在源码中何去何从。

总的来说,ViewGroup 和 View 属于树的结构,事件分发就是从父节点到子节点一步
步遍历的过程,直到找到可以消费事件的 View 为止。而在这一过程中,就是一个不断
递归调用以上伪代码的过程。子 View 总是在经过 dispatchTouchEvent 的执行后将
返回值交给父 View,父 View 根据子 View 是否消费了事件再确定自己是否需要消费
事件,然后再向自己的父 View 返回一个表示自己或者自己的子 View 是否消费了事件
的布尔值。如果当前 View(ViewGroup)自己或者子 View 不消费就会将事件转给父
View 的 onTouchEvent 方法,所以就会呈现了上面的 U 型图。分析了源码之后,更
能体会这其中设计的精妙
关于事件分发源码我认为最好还是模拟一个事件流,跟着代码走,努力避开各种其他代
码的干扰(例如安全检查等),并且从最简单的事件入手,即单指触模、控件不进行滑
动、先不考虑 ACTION_CANCEL 事件。
本文基于 Android23 的源码进行分析。请对照 View 和 ViewGroup 的源码来看本文
首先要明确一个事件序列指的是从手指按下、滑动、抬起的这一个过程中的多个事件。
分别是 DOWN、MOVE、UP 事件。
在这里,假设一个情景,有一个 Activity,里面的布局是最外层一个 ViewGroup A(充
满屏幕),A 里面有个 ViewGroup B(充满 A),B 里面有个 Button C(比如在屏幕
中间)。
情形 1:
A完全(即A上的所有点都要拦截)拦截事件(即InterceptTouchEvent直接返回true),
现在单指点击了一下 A 范围任意点,然后滑动并抬起。
(Activity、PhoneWindow 、DecorView 的分发就不进行分析了)
现在的事件为 ACTION_DOWN。
首先 DecorView 调用了 ViewGroup A 的 dispatchTouchEvent 方法(当然这里只挑
关键代码),看到 ViewGroup 的 2103 行:

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//先判断是否允许该 ViewGroup 拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLO
W_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;
}

这一段很简单,但是很重要。关键在 intercepted = onInterceptTouchEvent(ev);,
大家也很熟悉,此时 A 要拦截事件的,所以 intercepted 为 true。
于是,2134 行的:

if (!canceled && !intercepted)

里面的语句就不会被执行(里面的语句主要是遍历子 View 找到消费这一系列事件的子
View)

然后来到 2239 行:

// 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);
}
mFirstTouchTarget 就是找到的那个要消费事件的子 View,此时因为根本没有遍历过
子 View 去寻找,所以为 null,所以调用:
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);


这里 dispatchTransformedTouchEvent 很重要,还要注意第三个参数传了 null。
进入 dispatchTransformedTouchEvent 方法,先看下注释:

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.

最后一句话说的很明白了,如果 child(即第三个参数)为 null,则该事件会传给当前的 ViewGroup,即 A。

对应的代码在 dispatchTransformedTouchEvent 方法中,处于 ViewGroup 的 2567
行:

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

是的,现在调用了
handled = super.dispatchTouchEvent(transformedEvent);
其实就是 View 的 dispatchTouchEvent 方法。
在 View 的 dispatchTouchEvent 中,关键看 View 的 9285 行:

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


可以看到如果 View 已经设置了 onTouchListener 并且返回 true,则 onTouchEvent
并不会被执行,并且整个 dispatchTouchEvent 方法最后也是返回这个 result。
View 默认 onTouchListener 为 null,所以 onTouchEvent 会被执行。
onTouchEvent 中默认主要是对 OnClickListener 和 OnLongClickListener 等事件的
处理。看源码 View 的 10288 行:

if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)

这里的判断语句如果条件不成立,即假如 View 不是 Clickable(默认状态)的话,
onTouchEvent 则返回 false,当然也不执行各种点击事件。
此时如果 A 是默认状态,则 A 的 dispatchTouchEvent 返回 false 给 DecorView,它
的意义是:A 本身和子 View 都不消耗该事件。因为 DecorView 只有一个子 View A,
如果 A 不消费事件,那么在这一系列事件的后续事件,及 MOVE、UP,就不会分发给
A 了。
如果 A 是 Clickable 的(比如被 set 了 OnClickListenrer),则 onTouchEvent 返回
true,那么返回 true 给 DecorView,DecorView 就会将后续的事件都传给它处理。而
在 ACTION_UP 事件传递过来的时候,onTouchEvent 就会触发 OnClickListenrer 等
的点击事件。
至于为什么,且听后面分解。
在这里,可以知道,在 ViewGroup 拦截事件的情况下,会通过
dispatchTransformedTouchEvent去调用自己的super.dispatchTouchEvent方法最
后调用 onTouchEvent 方法,也就是把自己当做一个 View 处理事件。
dispatchTransformedTouchEvent 很关键,具体下面会说明~~
情形 2:
A 不拦截事件,B 拦截事件,单指点击屏幕任意点,然后滑动并抬起。
同样,首先 DecorView 调用了 ViewGroup A 的 dispatchTouchEvent 方法,这时候
intercepted 已经是 false 了,所以到了 ViewGroup 的 2134 行的判断

if (!canceled && !intercepted)

就需要进入了(此时的 cancel 为 false,一般情况下都为 false)。
此时来到 ViewGroup 的 2144 行:

if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINT
ER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE)
{

目前只看第一个判断 actionMasked == MotionEvent.ACTION_DOWN,其实这个判
断语句内部是用于找出能够消费 Down 事件的子 View,这个对于此次系列的事件意义
重大,我们进入判断语句看。
首先看从 ViewGroup 的 2155 行开始的这一段:

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.
//从顶到底的子 View 集合
final ArrayList<View> preorderedList = buildOrderedChildLis
t();
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(child
Index);
// If there is a view that has accessibility focus we w
ant it
// to get the event first and if not handled we will per
form 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 (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child,
null)) {
ev.setTargetAccessibilityFocus(false);
//如果 View 不包含触摸点则继续遍历
continue;
}

很明显,ViewGroup 在遍历子 View,buildOrderedChildList 方法这里是创建了一个
从顶到底的子 View 集合去遍历,所以越顶部的 View 越优先可以消费事件。最后主要
是使用 isTransformedTouchPointInView 方法判断触摸点是否在子 View 上。
B 是 A 的子 View,并且触摸点在 B 上,所以不会执行 continue,即继续执行后面的代
码。
来到 ViewGroup 的 2199 行:

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 ori
ginal 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, idBitsTo
Assign);
alreadyDispatchedToNewTouchTarget = true;
break;
}

又见到了 isTransformedTouchPointInView 方法,
进入该方法,跳过关于 ACTION_CANCEL 以及多指触控的代码,主要就是一下代码:

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

这里主要是分 child 是否为 null 两种情况,在这里先不讨论 null 的情况,这里的 child
为通过遍历且确定触摸点在其中的 View。
当 child 不为 null 的时候,先做一些滑动偏移量处理,然后调用 child 的
dispatchTouchEvent 方法。
是的,这里就是真正实现事件分发的地方,开始将事件传递到子 View 的
dispatchTouchEvent 方法。在已经确定触摸点在 View 上的情况下,调用
dispatchTransformedTouchEvent 方法的作用是通过自 View 的
dispatchTouchEvent 的返回值来判断该 View 是否要消费这个事件,true 则为消费,
false 则不消费。
现在子View为ViewGroup B,B是拦截事件的,所以interceptTouchEvent返回true,
所有 B 也会像情形 1 中的 A 一样执行 View 的 dispatchTouchEvent 方法,再调用
onTouchEvent 方法。默认情况 onTouchEvent 返回 false,所以不会走入前面 A 的

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

里面的代码,而是会走到 ViewGroup 2239 行的代码:

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

是的,这里的 mFirstTouchTarget 是在

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

判断语句内才会被赋值,使得它持有消费了事件的 View。
如果 B 的 onTouchEvent 返回 true,即 B 消费事件的情况下才会被赋值,它本身为一
个 ViewGroup 内部类 TouchTarget 的链表,之所以为链表主要是为了多指触摸情况,
这里我们暂且认为它代表的是能够消费该事件的 View 就可以。
走到这里是已经遍历完所有 A 的子 View ,如果遍历完发现没有子 View 消费事件
(mFirstTouchTarget == null),则调用:

handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);

注意到此时 child 参数传 null,回看 dispatchTransformedTouchEvent 的代码就知道
会调用 super.dispatchTouchEvent,源码中有关于此的注释:
前面已经说过了,如果没有子 View 消费,当前 ViewGroup 就自己调用
dispatchTouchEvent 尝试去消费。
如果 B 可以消费事件呢(B 的 onTouchEvent 返回 true)?那么在 A 遍历到 B 的时候,
A 的判断语句中的 dispatchTransformedTouchEvent 就会返回 true(当此时 B 已经
执行完 onTouchEvent 方法),那么就会执行前面列出来的,ViewGroup 的 2201 行
开始的代码。
重点是 ViewGroup 的 2215 行:

newTouchTarget = addTouchTarget(child, idBitsToAssign);

看下 addTouchTarget 方法:

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

其实就是在以 mFirstTouchTarget 为头的链表插入一个新的头节点。不考虑多指触摸
情况,就是类似赋值 child 给 mFirstTouchTarget 持有的意思。
这里很关键,mFirstTouchTarget 被赋值了,保存的是 A 中需要消费事件的子 View,
然后在 dispatchTouchEvent 剩下的代码中,会在 mFirstTouchTarget 不为 null 的情
况下,返回 true,向 DecorView 报告 A 的子 View 或自己有 View 消费事件。
mFirstTouchTarget 有什么意义呢?记得事件分发中有一条规则:
一旦一个 View 消费了 DOWN 事件,那么该系列的后续事件都由该 View 处理。
现在 DOWN 事件结束了,来了 MOVE 事件。
同样的 A 的 dispatchTouchEvent 方法,又来到 ViewGroup 2144 行的:

if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINT
ER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE)

判断语句,这次是 ACTION_MOVE 事件,当然无法进入语句内部,于是乎 A 遍历子
View 找出能够消费事件的 View 都没有执行,直接跳到前面提到的 ViewGroup 的
2240 行的代码,然后进入 2244 行的 else 语句,其中关键是 2251 行代码:

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.c
hild)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}

alreadyDispatchedToNewTouchTarget 表示是否是新添加 TouchTarget 的,这个
在上一个事件 DOWN 的时候是被置为 true,但是在这次事件中由于没有添加新的
TouchTarget,所以为 false。
所以会走到 2256 行:

if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}

这里的 target.child就是 mFirstTouchTarget 中持有的 View,在这里就是ViewGroup
B。所以通过 dispatchTransformedTouchEvent 我们知道这里将当前事件
ACTION_MOVE 传给了 B 的 dispatchTouchEvent 方法。
总的来说就是通过 mFirstTouchTarget 保存 DOWN 事件的消费 View B,然后在后
续的事件直接传给了 B 的 dispatchTouchEvent 处理。

此时如果 B 要消费这个 MOVE 事件,则 handle 赋值为 true,则 A 的
dispatchTouchEvent 返回 handle 为 true。如果 B 不要消费这个 MOVE 事件,那么
A 的 dispatchTouchEvent 返回 false 给 DecorView。
情形 3:
A 不拦截事件,B 也不拦截事件,单指点击 Button,然后滑动并抬起。
其实和情形 2 很相似了。
首先是 Down 事件,DecorView 调用 A 的 dispatchTouchEvent 方法,A 因为
onInterceptTouchEvent 返回 false,遍历点击到的子 View 找到 B,调用了 B 的
dispatchTouchEvent 方法,B 因为 onInterceptTouchEvent 返回 false,遍历子 View
找到Button C,Button因为本身为Clickable,所以dispatchTouchEvent方法返回true
给 B,所以 B 将 C 记录在 B 的 mFirstTouchTarget 中,然后 B 的 dispatchTouchEvent
返回 true 给 A,告诉它“我这边可以处理这个事件”,然后 A 将 B 记录在 A 的
mFirstTouchTarget 中,A 的 dispatchTouchEvent 方法返回 true 给 DecorView。
于是等到 MOVE 事件下来,A 直接找 A 的 mFirstTouchTarget 持有的 View,即 B,B
直接找 B 的 mFirstTouchTarget 持有的 View C,如果 C 要消费这个事件那还是一路往上返回 true。
那如果 C 这时候不要消费事件呢?那么 C 的 dispatchTouchEvent 返回 false,B 的dispatchTouchEvent 返回 false,且还不能调用自己的 super.dispatchTouchEvent处理,所以又将 false 返回给 A,A 也和 B 一样,所以因为递归最终这个事件交给了Activity 处理。

这就是事件分发规则中:
“如果 View 不消耗 DOWN 以外的其他事件,则父 View 不会调用 onTouchEvent
处理这个事件,同时该 View 仍然可以继续接收到后续的事件,这些 View 不处理的事件都交给 Activtiy 处理”。
那如果 View 不消耗 DOWN 事件呢?其实前面情形 1 已经简单说过了,这里结合 A,B,C以及后面两个情形一起来说会更加直观。就是 C 如果不接受 DOWN 事件,那么 B 的onTouchEvent 方法会处理事件(不考虑 onTouchListener 情况),如果 B 不可以消费 DOWN 事件,则调用 A 的 onTouchEvent 方法处理。所以出现了 U 型图那样将事件往上抛的情形。
如果 B 可以消费 DOWN 事件,那么 C 的mFirstTouchTarget 就记录为 B,此时 B 的 mFirstTouchTarget 为 null,所以后续事件来到 C 后,C 直接交给了 B,B 因为事件不是 ACTION_DOWN 了,所以不会遍历子View,直接判断 mFirstTouchTarget 是否为 null。由于没有遍历子 View,所以mFirstTouchTarget 仍然为 null,所以 B 会调用自己的 super.dispatchTouchEvent处理,以之前分析的类推,后续事件不会被 C 接收(这部分可以重看下 ViewGroup 的2144 行的判断语句)。
这就对应了另一个事件分发的规律:
一旦一个 View 在 onTouchEvent 中不消耗事件,则后续的事件都不会交给他来处理。
(看源码发现如果 DOWN 触摸点在多个 View 中,应该说是这几个 View 都不消耗事
件为前提?)

最后,当一个系列事件结束之后,新的系列事件来到的时候,会将上一次的 保存的状
态清空(比如 mFirstTouchTarget),看 ViewGroup 的 dispatchTouchEvent 在
ViewGroup 中的 2094 行:

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gestur
e.
// The framework may have dropped the up or cancel event for th
e previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}

关于事件分发机制就先简单说到这里,还有很多东西没有提及,比如多指触摸、CANCEL
事件等。事件分发由于涉及递归,有时候一层层进入又一层层出来很容易让人迷路。我
也是对源码研究不深入,难免有疏漏或者错误的地方,望各位指正~~

上一篇下一篇

猜你喜欢

热点阅读