概述
-
Touch 事件的分发是从上到下,从父到子:
Activity
->ViewGroup1
->ViewGroup1
的子ViewGroup2
-> ... ->TargetView
-
Touch 事件的响应是从下到上,从子到父:
TargetView
-> ... ->ViewGroup1
的子ViewGroup2
->ViewGroup1
->Activity
boolean dispatchTouchEvent(MotionEvent ev)
事件分发的处理函数,事件来自 Choreographer
,开始分发的地方是 Activity
,Activity
根据 UI 显示的情况,把事件传递给相应的 ViewGroup
。该方法返回 true
表示事件不再往下分发,由于没有找到目标 View
,因此也就表示将该事件丢弃。
开发者通常不需要重写该方法。除非是自定义
ViewGroup
,需要自定义分发顺序
boolean onInterceptTouchEvent(MotionEvent ev)
事件的拦截函数,这个是暴露给开发者根据自身需求需要重写的方法,返回 true
表示拦截。对不同事件的拦截,行为上有差异的表现:
- 如果
ACTION_DOWN
事件没有被拦截,顺利找到了TargetView
,那么后续的MOVE
和UP
都能经由该函数下发。如果后续的MOVE
和UP
下发时还有继续拦截的话,事件就只能传递到拦截层,并且向TargetView
发出ACTION_CANCEL
- 如果
DOWN
事件下发时就被拦截,且拦截层没有消费事件,导致没有找到TargetView
,那么后续的MOVE
和UP
都无法向下派发了,在Activity
层就终止。
boolean onTouchEvent(MotionEvent ev)
响应处理函数,如果有设置对应 listener
的话,这里还会与 onTouch
,onClick
,onLongClick
有关。执行顺序是 onTouch
-> onTouchEvent
-> onClick
-> onLongClick
。返回 true
表示被处理。该函数返回 true
表示事件被消费
requestDisallowInterceptTouchEvent 在 dispatchTouchEvent 中的应用
有时候我们不希望父 View
拦截事件,比如出现滑动冲突的时候,安卓系统给我们提供了 requestDisallowInterceptTouchEvent()
方法来通知父 View
不要拦截事件。使用这个方法的正确姿势,网上都是这样写的:
public boolean onTouchEvent(MotionEventevent){
switch(event.getAction()){
// 这里是解决滑动冲突的例子,一般来说滑动冲突都是因为父 View
// 在收到滑动一定距离后,就在 onInterceptTouchEvent 中将 MOVE
// 事件也给拦截掉了。因此才导致子 View 就滑动不了了。所以这里在
// 子 View 还能收到 MOVE 事件时,就通知父 View 不要拦截。
// 其实确定希望父 View 永远不要拦截的话,那么在 DOWN 中
// requestDisallowInterceptTouchEvent() 也可以。
case MotionEvent.ACTION_MOVE:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
parent.requestDisallowInterceptTouchEvent(false);
break;
}
}
那么它是如何让父 View
不再拦截事件的呢?我们来看下 dispatchTouchEvent()
中涉及这部分的源码。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (actionMasked == MotionEvent.ACTION_DOWN) {
resetTouchState();
}
// 下面判断是否符合调用 onInterceptTouchEvent() 的条件
final boolean intercepted;
// 当前是 DOWN 事件,或者已经有目标 View
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 这里 DOWN 事件时,disallowIntercept 一定为 false。
// 因此 DOWN 事件时,onInterceptTouchEvent() 一定会被调
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
// 既不是 DOWN 事件,又没有目标 View 时(什么时候会出现这种情况?)
intercepted = true;
}
}
...
}
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
- 当父
View
收到DOWN
事件时,会首先调用resetTouchState()
方法将FLAG_DISALLOW_INTERCEPT
标志位清零,这就是为什么子View
如果在构造方法中调用requestDisallowInterceptTouchEvent()
没有用的原因,子View
最早都要在收到DOWN
事件后调用requestDisallowInterceptTouchEvent()
才会起作用。 - 当第一次的 DOWN 事件下发时,父
View
一定会调用一次onInterceptTouchEvent()
。如果这时父View
进行了拦截,那么子View
就永远也收不到任何事件,也就没有机会调用requestDisallowInterceptTouchEvent()
。
事件如何由父 View 分发给子 View 的
我们还是看父 View.dispatchTouchEvent()
。总体上,父 View
是以广度遍历的形式来将事件分发给各个子 View
的:
// 不是 CANCEL 并且不拦截
if (!canceled && !intercepted) {
// 在 DOWN 时去寻找目标 view
if (actionMasked == MotionEvent.ACTION_DOWN) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
...
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); // 这里就是从 childView 数组中取得一个 View,只不过综合了一个自定义 Child 排序,这里先不管
// 如果坐标不在 child 的范围内,则跳过该 child
if (!child.canReceivePointerEvents() || !isTransforedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
...
// 这里将 DOWN 事件传给 child(该调用在 DOWN 事件的条件分支里),调用 child 的 dispatchTouchEvent() 方法去分发
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
// 经过上面的逻辑后,只存在下面两种可能:
// 1. 没有找到目标 view。那么该 ViewGroup 就看做普通 View:直接调用 super.dispatchTouchEvent(),向上回调
// 2. 找到了目标 view,那么后续事件都可以直接到目标 view
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); // 注意这里第三个参数是 child,传的是 null,这会导致方法中直接调用 super.dispatchTouchEvent()
} else {
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// 如果前面 DOWN 找到目标 view 时已经消费掉 DOWN 事件,那么这里直接返回 true
handled = true;
} else {
// 如果不是 DOWN 事件,又被拦截了,则需要向之前的事件传递链上的 child 发送 CANCEL 事件
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
// 把事件传递给目标 view 或者 传递 CANCEL 事件给目标 view
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
handled = true;
}
}
}
if (canceled || actionMasked == MotionEvent.ACTION_UP) {
// 如果是 CANCEL 或者 UP,重置 Touch 状态标识,mFirstTouchTarget 赋值为 null,后续的事件无法派发给 子 View 了。
resetTouchState();
}
}
子 View 在哪里消费事件,以及如何通知上层父 View 的?
我们再看上面的代码,在广度遍历各个子 View
时,调用了 dispatchTransformedTouchEvent()
方法:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
// 处理 CANCEL
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) { // 前面事件分发时,没有找到目标 view,则调用 super.dispatchTouchEvent(),由于本身是 ViewGroup,所以 super 就是 View,也就是将这个 ViewGroup 当成普通 View 来处理。
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
// 普通事件,跟上面处理 CANCEL 一样,只不过 CANCEL 是往下派发 CANCEL。或者自己处理 CANCEL
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);
}
return handled;
}
无论事件怎么派发,最终都是派发到了叶子 View
上,或者没有子 View
的话,通过 super.dispatchTouchEvent()
将自己当成一个 View
(而不是 ViewGroup
) 来处理。我们看 View
的 dispatchTouchEvent()
:
public boolean dispatchTouchEvent(MotionEvent event) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
...
return result;
}
从上面可以看到,叶子 View
的 dispatchTouchEvent()
中就没有 OnInterceptTouchEvent()
了,取而代之的是 OnTouchListener
和 OnTouchEvent
,而且如果 OnTouchListener
先消费事件的话,那么 onTouchEvent
就不会再调用。
我们再看 OnTouchEvent()
中做了什么,代码有点多,我们就捡几个重点说一下:
-
performClick()
是在ACTION_UP
中进行的。
下面我们用 demo 来看看事件的具体分发,View 的层级关系依次是 MainActivity
-> ParentLayout
-> ChildLayout
-> InnerButton
Case 0:正常下发事件
image.pngCase 1: DOWN 在任意层被抛弃
Case 1.1 抛弃后,后续事件不被拦截
在 ChildLayout 层被抛弃,且后序事件不被拦截
image.png-
DOWN
在ChildLayout
被抛弃 - 之后事件走正常下发流程,而且只会分发到抛弃层,即
ChildLayout
。并且将ChildLayout
当做普通View
来看待,即不会调用它的onIntercept()
。后续事件回传时,中间层的onTouch()
也不会被调用,但是会调用Activity.onTouch()
在 Activity 层被抛弃
image.png- 本质上跟上面是一样的。
Case 1.2: 抛弃后,后续事件被中间层拦截
image.png-
DOWN
事件在哪一层被抛弃,系统就认为那一层是暂时的目标View
,因此后续事件会走正常流程下发到抛弃层 - 后序事件下发过程中,中间层可拦截,由于有目标
View
,因此中间层拦截后,会向下往目标View
发送CANCEL
:- 如果没有
View
消费CNCEL
,则Activity.onTouch()
会收到MOVE
,但是第一个拦截事件不会调用拦截层及其他中间层的onTouch()
- 如果有
View
消费CANCEL
,则往上回调时,不会有任何其他的onTouch()
会被调用。
- 如果没有
- 拦截事件后的所有事件,都会将拦截层当做新的目标
View
,事件下发到拦截层。
- 说明被抛弃并不会改变目标
View
,但是被拦截就会改变目标View
。- 被抛弃并不会触发
CANCEL
事件的下发,但是拦截中间事件触发CANCEL
发向目标View
- 一旦有
onTouch()
被调用,那么回溯时就不会再有onTouch()
被调用。- 在任意层调用过
onTouch()
,且事件未被消费,则Activity.onTouch()
一定会被调用
Case 2: DOWN 被消费
Case 2.1:DOWN 被 InnerButton 消费,后序事件在 ChildLayout 层被抛弃
image.png-
DOWN
被InnerButton
消费没什么好说的,走正常流程。 -
MOVE
被ChildLayout
抛弃,被抛弃前走正常流程 - 之后事件 及
UP
走正常流程
Case 2.2: DWON 被 InnerButton 消费,后续事件在 ChildLayout 层被拦截
image.png-
DOWN
被InnerButton
消费没什么好说的,走正常流程 -
MOVE
被ChildLayout
拦截,由于之前DOWN
已经被消费,因此有目标View
,向目标View
发送CANCEL
. - 之后的事件只下发到新的目标
ChildLayout
Case 3: DOWN 被中间层拦截
case 3.1: DOWN 被 ChildLayout 拦截,且被消费
image.png
- 在还没有目标
View
时,拦截事件,会马上调用拦截层的onTouch()
。但是如果已经有目标View
时,拦截事件,不会调用拦截层的onTouch()
,但是会向目标View
发送CANCEL
事件。后续事件才会将拦截层当做目标View
。
case 3.2: DOWN 被 ChildLayout 拦截,且未被消费
image.png-
DOWN
在下发时被ChildLayout
拦截。由于此时还没有目标View
,因此马上调用拦截层的onTouch()
。ChildLayout.onTouch()
也不消费事件,因此层层往上回溯onTouch()
直到Activity
。 - 由于没有目标
View
,因此之后的所有事件都只能到达Activity
层
网友评论