Android的事件分发机制也是老生常谈了,本文从细节入手解读一下整个机制中的几个重要部分。
Android中touch事件一定是从ACTION_DOWN开始,所以ACTION_DOWN的处理至关重要,我们先来看看ACTION_DOWN这个事件相关的细节。
说到Android事件分发,一定绕不开 dispatchTouchEvent 函数,View和ViewGroup的该函数有很大的不同。
我们来看看ViewGroup的 dispatchTouchEvent 函数,它的部分源码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
...
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
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;
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
...
for (int i = childrenCount - 1; i >= 0; i--) {
...
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
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();
}
...
}
}
// 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;
}
...
}
predecessor = target;
target = next;
}
}
...
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
可以看到整个分发有几个关键因素:
intercepted、canceled比较好理解,重点来说说后面两个因素的是如何影响整个分发的。
一个完整的事件应该包含
其中ACTION_DOWN是开始也是关键。
从上面dispatchTouchEvent
源码中可以看到首先单独对ACTION_DOWN事件进行了处理,对所有child进行遍历,是从后向前遍历的,所以在处理上面的也就是最后添加的view会先得到事件
for (int i = childrenCount - 1; i >= 0; i--) {
对于每个child,会先判断事件是不是发生在它的区域内,不是则不处理:
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
如果在区域内,则继续执行,下面dispatchTransformedTouchEvent
这个函数就是下发事件的,我们来看下部分源码:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
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;
}
有不少逻辑在里面,但是仔细观察可以发现,不论那个条件,执行的代码都比较类似,如下:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
...
handled = child.dispatchTouchEvent(event);
...
}
当child不为null的时候,执行child的 dispatchTouchEvent ;为null时执行父类的dispatchTouchEvent,即View的dispatchTouchEvent函数,这个函数里会执行onTouchEvent等。所以在ViewGroup是没有onTouchEvent等函数的代码。
由于这时child不为null,所以执行了child的dispatchTouchEvent函数.
回到之前的ACTION_DOWN流程中,根据dispatchTransformedTouchEvent返回值进行不同的处理:
如果返回true,即有一个child消费了ACTION_DOWN事件,可以看到后续执行了addTouchTarget函数,同时将alreadyDispatchedToNewTouchTarget置为true。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
addTouchTarget函数源码如下:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget; //初始mFirstTouchTarget为null,所以这里next是null
mFirstTouchTarget = target;
return target;
}
关键的一点是对mFirstTouchTarget进行了赋值。所以说true的处理是为mFirstTouchTarget赋值,将alreadyDispatchedToNewTouchTarget置为true
最后的break则跳出循环,不再遍历其他child。
如果返回false,即没有任何一个child消费ACTION_DOWN事件,直接跳过if代码,这样mFirstTouchTarget为null。
那么mFirstTouchTarget、alreadyDispatchedToNewTouchTarget这两个属性在分发过程中的作用是什么?我们分别来说:
当mFirstTouchTarget为null,进入if语句执行dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
由于child是null,在dispatchTransformedTouchEvent代码中可以看到不再给任何child分发,而是调用了super.dispatchTouchEvent,即ViewGroup自己处理
这样ACTION_DOWN事件分发完了。其他事件分发时由于不再走ACTION_DOWN的处理过程,所以mFirstTouchTarget会一直为null,所以其他事件也不再向下分发了,直接ViewGroup自己处理
当mFirstTouchTarget不为null,进入else语句中,会执行一个while循环
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;
}
...
}
predecessor = target;
target = next;
}
}
这时由于alreadyDispatchedToNewTouchTarget为true,所以直接给handled赋值true并不做任何处理。因为之前代码中child对ACTION_DOWN事件已经响应,所以这里的alreadyDispatchedToNewTouchTarget是为了防止重复分发ACTION_DOWN事件。
这样ACTION_DOWN事件分发完成后,分发其他事件时,alreadyDispatchedToNewTouchTarget被重新赋值false,由于不再走ACTION_DOWN的处理过程,所以alreadyDispatchedToNewTouchTarget就一直是false了,而mFirstTouchTarget会一直保持不变。在这个while循环中则会执行else语句,通过执行dispatchTransformedTouchEvent将事件直接分发给mFirstTouchTarget对应的child,即之前消费ACTION_DOWN事件的child。
我们知道在事件分发过程中是存在一个拦截机制的:onInterceptTouchEvent
当它返回true则不向下分发事件,否则向下分发。
但是在这个过程中,还有一个参与者:requestDisallowInterceptTouchEvent,这个函数直接影响事件的拦截。我们今天就来说一说这个这个函数是如何影响事件分发的。
我们先看看这个函数的源码
@Override
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);
}
}
可以看到它改变了一个开关FLAG_DISALLOW_INTERCEPT,同时调用其parent的函数。
那么这个开关有什么用?
在ViewGroup的dispatchTouchEvent函数开头有这样一段代码:
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;
}
先来看判断逻辑,当是down事件或者mFirstTouchTarget不为空,则进入一个代码段;否者拦截设置为true。
我们知道down事件分发过程中,如果有子view消费事件,则赋值给mFirstTouchTarget,后续事件会直接分发给mFirstTouchTarget
这里也可以看出,如果有子View消费了down事件,即mFirstTouchTarget不为空,所以后续事件还会检查拦截。
所以上面就可以理解了,如果down事件中没有子view消费事件,那么后续事件的拦截都为true。所以后续事件不会再遍历子View。
下面再看if代码段
一开始就使用了FLAG_DISALLOW_INTERCEPT开关,即disallowIntercept
所以简单来说requestDisallowInterceptTouchEvent设置为true可以跳过onInterceptTouchEvent,不拦截事件。
而且因为requestDisallowInterceptTouchEvent又调用了parent的函数,所以所有层次的父view都不再拦截。
所以requestDisallowInterceptTouchEvent 的功能是让这个view及上面的所有父view都放开拦截,即使onInterceptTouchEvent为true。
所以我们一般如下使用
view.getParent().requestDisallowInterceptTouchEvent(true);
这样view的所有层次的父view都不会拦截事件了。
下面让我们再深入想想。上面这种的情况是在touch事件发生前设置onInterceptTouchEvent,也是我们一般的用法。但是如果事件发生过程中调用这个函数呢?
比如在view的onTouch的某个事件中使用
getParent().requestDisallowInterceptTouchEvent(true)
当事件开始分发时,down事件进入父view的dispatchTouchEvent时,这是子view还未得到事件,所以没有设置requestDisallowInterceptTouchEvent。
这时如果父view的onInterceptTouchEvent返回true,即拦截的话,事件则不会分发给子view了,所以requestDisallowInterceptTouchEvent永远不会执行,子view则无法得到事件。
但是如果父view的onInterceptTouchEvent返回false,即不拦截的话,事件就可以分发到子view,requestDisallowInterceptTouchEvent执行,之后的事件都会跳过父view的onInterceptTouchEvent的判断
例如父view的onInterceptTouchEvent代码如下
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return false;
case MotionEvent.ACTION_MOVE:
return true;
case MotionEvent.ACTION_UP:
return true;
default:
break;
}
return false;
}
down事件不进行拦截,但是拦截了move和up事件。
如果子view的onTouch的down事件中使用
getParent().requestDisallowInterceptTouchEvent(true)
这样down事件分发到了子view,执行了requestDisallowInterceptTouchEvent,同时返回了true。随后move或up事件分发到父view时,因为被设置了FLAG_DISALLOW_INTERCEPT标签,所以就会跳过onInterceptTouchEvent。
所以onInterceptTouchEvent中move和up的返回值设置就无效了,因为根本就不再执行这个函数了。
通过上面的我们得到几个结论:
ViewGroup分发事件down的时候,会遍历自己的子view,从前面的到后面的
for (int i = childrenCount - 1; i >= 0; i–) {
然后判断子view的区域是否包含事件,如果包含则进行处理。
所以同级分发时,即两个同级的view叠加在一起时,先分发给前面的view。
如果所有的child都不消费ACTION_DOWN事件,那么实际上child并不是收不到任何事件,而是ACTION_DOWN会分发给所有有效范围内的child,但是其他事件就不再分发了。
如果有一个child消费了ACTION_DOWN事件,那么后续的事件会直接分发给这个child,不再经过其他child。但是注意,在分发ACTION_DOWN事件时,排在这child前面的child还是会分发到ACTION_DOWN事件,但是也仅仅是ACTION_DOWN事件。
所以整个Touch事件分发过程中,ACTION_DOWN是至关重要的,我们通常考虑的返回值或继续分发的问题,实际上都是讨论ACTION_DOWN这个事件的,基本上ACTION_DOWN事件分发确定了,后续事件的分发就基本确定下来了。但是注意在后续的事件中,依然需要判断InterceptTouchEvent。
关于拦截通过上面的分析可以知道requestDisallowInterceptTouchEvent会让父view放开拦截,并且是向上层层生效的。同时我们也可以通过一些逻辑控制,使requestDisallowInterceptTouchEvent只作用在部分情况下。
那么你们有没有考虑一个问题,Android中的事件又是从哪来的,怎么分发到Activity的呢?关于这个可以看我的另外一篇文章《【Android开发】事件是如何分发到Activity的?》