聊一聊Android中的事件传递
文章中所有代码均是不完整的代码,旨在说明具体逻辑,具体详细代码需参考sdk
我们知道一个事件是首先会传递到Activity:
public class Activity{
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
...
return false;
}
}
Activity#dispatchTouchEvent
:这个方法会被调用用于处理屏幕touch事件,MotionEvent
代表了屏幕touch事件,返回值
:如果事件被消费了返回 true,否则返回 false。 这个方法干了两件事:
1、把touch事件传递给Window去处理
2、如果事件上一步没有人消费(整个View数没有view去消费这个事件),则回调 Activity#onTouchEvent
去处理。
Activity#onTouchEvent
:当Activity中所有的View都不对此次事件进行处理时(事件没有被消费),会调用这个方法。返回值
:如果Activity消费了事件则返回 true,否则返回 false,默认返回 false
getWindow().superDispatchTouchEvent(ev)
会把事件传递给 Window
,Window
会把事件传递给 DecorView
,调用DecorView#dispatchTouchEvent
。DecorView
是View树的顶层View,它是一个 ViewGroup
。
小结:至此我们知道,一个touch事件来了以后会首先传递给View树顶层View:DecorView。如果整个View树都不对此次事件进行消费的话,那么会看Activity有没有想法想对事件进行消费。View树的组织形式采用了组合设计模式,树的节点元素是:ViewGroup 和 View。 接下来我们从代码上看一下 View 和 ViewGroup 是如何对 touch 事件进行响应的。
代码
代码部分:View 和 ViewGroup
View
public class View{
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
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;
}
return result;
}
}
View#dispatchTouchEvent
:把touch事件传递给目标View,自己本身也可能是目标View。返回值:
如果touch事件被这个View消费了返回 true,否则返回 false。处理逻辑:
1、如果此View设定了onTouchListener,且其onTouch方法返回 true
表示此View消费了事件。
2、如果View的onTouchEvent返回 true
表示消费了事件。
其它情况则表示未消费此事件。
我们分析一下View 的 onTouchEvent方法:
public class View{
public boolean onTouchEvent(MotionEvent event) {
...
//是否可点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable){
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
...
performClickInternal(); //调用mOnClickListener.onClick方法
}
break;
case MotionEvent.ACTION_DOWN:
...
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
case MotionEvent.ACTION_CANCEL:
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
// Be lenient about moving outside of buttons
if (!pointInView(x, y, touchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
}
break;
}
return true; // 注意返回
}
return false; //注意返回
}
}
View#onTouchEvent
:如果判断此View是可点击的,则返回true,表示消费事件,否则返回false,表示不消费事件。(如果View设置过 OnClickListener 或者 OnLongClickListener 则自动设置为CLICKABLE 或 LONG_CLICKABLE)。当clickable为true时,表示此view即为目标View,会消费事件。当消费事件时,View又会针对不同的事件类型完成不同的处理工作:
- ACTION_DOWN:
在每一个touch事件周期的ACTION_DOWN
中都会向主线程的消息队列中发送一个消息,并且延迟500ms执行。这个消息具体干了什么事:
if (performLongClick(mX, mY)) { //如果消费了 LongClick事件
mHasPerformedLongPress = true; // 表示已经消费了LongClick事件
}
会调用performLongClick
中调用了 mOnLongClickListener.onLongClick(View.this);
就是如果onLongClick返回true,则代表已经消费了LongClick 事件
- ACTION_UP:
如果 LongClick 长按事件还没有消费,则接下来会处理Click事件,调用mOnClickListener.onClick方法。同时会删除 ACTION_DOWN 中发送到主线程消息队列中的检测长按事件的消息。
如果 LongClick 长按事件已经被消费,就不会执行 Click 事件了。
一个touch事件具体是 Click事件 还是 LongClick 事件,在按下去(ACTION_DOWN)的时候是无法知道的。
ViewGroup
ViewGroup继承自View,重写了 View 的 dispatchTouchEvent,接下来看一下 ViewGroup#dispatchTouchEvent
的实现逻辑:
public class ViewGroup{
// First touch target in the linked list of touch targets.
@UnsupportedAppUsage
private TouchTarget mFirstTouchTarget; // 记录了是哪个子View消费了事件
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
// 检查是否打断
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
...
if (!disallowIntercept) { // requestDisallowInterceptTouchEvent 设置了以后全局控制
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.
// 什么情况下直接走这里呢?当ACTION_DOWN没有View进行消费,最后mFirstTouchTarget 就null。
// 这时候有ACTION_UP来的时候就是这种情况,此时就会直接自己的 onTouchEvent,不会再往下传递了。 又或者 ACTION_DOWN -> ACTION_MOVE 被拦截,之后 ACTION_UP 时也是这种情况。
intercepted = true;
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL; // CANCEL事件
// 没有取消 && 没有打断
if (!canceled && !intercepted) {
// ACTION_DOWN的时候才会去遍历所有子View寻找目标View
if (actionMasked == MotionEvent.ACTION_DOWN
|| ...) {
// 遍历子View,查看是否有子View是否消费了此次事件
for (int i = childrenCount - 1; i >= 0; i--) {
// 如果子view 不在事件的坐标范围,则遍历下一个子View
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
...
break;
}
}
}
// Dispatch to touch targets. 把事件分发给target View
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view. 把事件分发给自己。
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 如果 是ACTION_DOWN的情况已经做过事件分发了,不会再处理了,直接返回true
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true; // ACTION_DOWN 做过事件分发了
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 已经确定了target view,直接把事件分配下去
// 如果mFirstTouchTarget存在,但是intercepted又是true,表示刚开始ACTION_DOWN时子View确定要消费且父View不拦截,可是后续有时间过来的时候比如 ACTION_MOVE,此时父View表示拦截,就会给子View 发送 ACTION_CANCEL事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
// 清空 mFirstTouchTarget
}
...
}
}
target = next;
}
}
return handled;
}
// 把事件传递给 child 子View,调用 child.dispatchTouchEvent
// 如果 child 子View为空,则直接调用 ViewGroup super中定义的dispatchTouchEvent。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
// 如果需要cancel
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL); //该表事件类型
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event); // 通知子View父类接管了
}
event.setAction(oldAction);
return handled;
}
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
// 把确定消费此次事件的child包装成TouchTarget,然后记录在 ViewGroup 的mFirstTouchTarget属性中
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
ViewGroup#dispatchTouchEvent
重写了View的dispatchTouchEvent,实现了ViewGroup的事件分发逻辑。ViewGroup 中有一个属性 mFirstTouchTarget
,在ACTION_DOWN事件分发的过程中如果事件最终被ViewA消费,那么ViewA 到 DecorView 的View 树中,经过事件传递的每一个ViewGroup 都会记录此次事件最终被自己的哪一个子View消费了,这个字段就是 mFirstTouchTarget
,记录下来的目的是为了在接下来的 ACTION_MOVE 和 ACTION_DOWN 时直接可以找到目标View,直接把相应的事件分配给它们。这里有一个注意的点是:如果ACTION_DOWN事件来时,View树中的某一个ViewX确定要消费此次事件,此时 ViewX 的父容器 到 DecorView中 的mFirstTouchTarget
都不为空,可是后来,当ACTION_MOVE、ACTION_UP时,View树中的某一个ViewGroup表示对事件进行拦击,那么这个时候,就会根据 mFirstTouchTarget
回调下面的每一层View一个事件 ACTION_CANCEL,并且清空之后的每一层View中 mFirstTouchTarget
的值。
ViewGroup#dispatchTouchEvent
的代码实现还是有点复杂,考虑了各种组合情况,调用逻辑采用递归的形式。大概分为下面几部分:
- 检查是否拦截调用 onInterceptTouchEvent
- 如果没有被拦截 且 不是ACTION_CANCEL事件,如果事件是 ACTION_DOWN则遍历ViewGroup自己的子View(因为ACTION_DOWN之后,要么没有消费对象,要么有View消费,这两种情况对于以后的事件都不需要再遍历了),调用子View的dispatchTouchEvent,利用递归实现同样逻辑。然后如果递归的期间发现有View消费了事件,则每一层都会保留下一层消费了事件的子View。
- 最后一步进行事件分发(这里有个注意的地方,在执行ACTION_DOWN时,之前已经遍历分发过了,所以这里会做排除)。
mFirstTouchTarget
为空的,调用自己的 onTouchEvent。 不为空mFirstTouchTarget
则直接通知其进行事件传递。