1. 简介
在Android开发中,事件分发是比较重要的基础知识,了解并熟悉整套机制有助于更好的分析各种点击滑动失效问题,也更有利于去扩展控件的事件功能和开发自定义控件。
事件分发中事件指的是一次完整的点击中所包含的事件(如 手指按下屏幕、手指在屏幕中移动、手指抬起等);分发指的是事件从Activity到Window再到ViewGroup最后到View的捕获阶段以及逆方向消费事件的冒泡阶段。
首先我给出一张事件分发的流程图,看过之后能对事件分发有一个大概的了解。
在此,先对整个流程图进行大体上的描述:
- 点击事件首先被Activity捕获,接着一路向下分发,直到被ViewGroup或者View的onTouchEvent消费。
- 事件向下传递的过程中会经过0或者多个ViewGroup。
- 当onTouchEvent返回true时,代表事件已经被处理了,流程结束了。
- 当onTouchEvent返回false时,事件就会向上冒泡给父视图处理。
- 本质上View没有参与事件分发,只是参与了事件处理。
- 上图是针对ACTION_DOWN事件的,ACTION_MOVE以及ACTION_UP事件会在后面分析
2. 源码分析
上图只是对事件分发的流程进行了简单的描述,想要分析各种点击滑动失效问题或者开发自定义控件,我们必须对深入了解事件分发的细节。
2.1 Activity
我们上文讲到点击事件首先被Activity捕获,我们先看Activity对点击事件的处理。
/**
* Activity
* 可以通过重写此方法截获所有Touch事件
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 默认为空实现,用于管理状态栏通知,非重点。
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev) /* 分析1 */) {
return true;
}
return onTouchEvent(ev)/* 分析2 */;
}
/**
* 分析1
* Activity
* 通过此方法获得Window对象,由于Window是一个抽象类,仅有一个PhoneWindow实现,
* 因此此处是拿到了一个PhoneWindow对象,后续就会走到PhoneWindow的superDispatchTouchEvent方法
*/
public Window getWindow() {
return mWindow;
}
/**
* 分析2
* Activity
* 当没有视图处理触摸屏事件时调用,若判断成立则关闭Activity
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event) /* 分析3 */) {
finish();
return true;
}
return false;
}
/**
* 分析3
* Window
* 通过源码可以看到,如果手指抬起时在边界之外 或者 事件本身就是出界事件则认为应当关闭Activity
*/
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
2.2 PhoneWindow
/**
* Window
* PhoneWindow仅仅是调用了DecorView的superDispatchTouchEvent方法
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
2.3 DecorView
/**
* DecorView
* DecorView又调用了父类的dispatchTouchEvent方法
* 由于DecorView继承自FrameLayout属于ViewGroup,因此此处调用了ViewGroup的dispatchTouchEvent
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
2.4 ViewGroup
由于ViewGroup以及View中方法的源码一般非常长,还涉及到一些在此处不太重要的细节,比如多指手势或者鼠标点击事件等,因此我们在下文讨论的源码都是经过简化的代码,用…代表省略的代码。
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// 处理初始Down事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 当开始一个新的触摸手势时,重置状态。
cancelAndClearTouchTargets(ev);
//分析1
resetTouchState();
}
// 拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 由分析1可以知道在没有别的设置项影响的情况下默认是允许拦截事件的 分析2
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 此处为判断是否拦截事件关键函数 分析3
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 没有touch事件的目标并且这个动作不是一个初始的down事件,继续拦截。
intercepted = true;
}
...
// 递归获取可获得焦点的子视图
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
...
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
// 获取子视图(可以自定义获取顺序)
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// 如果有一个视图具有可访问焦点,我们希望它先获取事件,如果未处理,将执行正常的调度。
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
// 此处令 i = childrenCount - 1 可以进行双重循环
// 第一重循环为 优先处理具有可访问焦点的视图
// 第二重循环为 正常调度所有子视图
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// 如果视图不能获得点击事件(可见且无动画)或者点击位置不在视图内 舍弃此View
if (!child.canReceivePointerEvents() /* 分析4 */
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 此时 mFirstTouchTarget 仍为null,getTouchTarget 的结果仍为null ,下面的分支不会走入
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
// 重置标志
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)/* 分析5 */) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// 通过预排序列表查找原始索引
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 通过addTouchTarget设置后mFirstTouchTarget不再为null 分析6
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
...
}
/**
* 分析1
* ViewGroup
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
// 此处位运算导致 mGroupFlags & FLAG_DISALLOW_INTERCEPT = 0
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
/**
* 分析2
* ViewGroup
* 见名知意,请求不允许拦截触摸事件,true则不允许拦截,false则可以拦截。
*/
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);
}
}
/**
* 分析3
* ViewGroup
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
...
// 默认为不拦截事件,可以在自定义组件时重写。
return false;
}
/**
* 分析4
* ViewGroup
*/
protected boolean canReceivePointerEvents() {
// 可见或者存在动画
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
/**
* 分析5
* ViewGroup
* 此方法本质上就是调用了子视图的dispatchTouchEvent方法
* 若子视图为空,则调用父类(View)的dispatchTouchEvent方法
* @Return 事件是否被消费
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
...
handled = child.dispatchTouchEvent(event);
...
}
return handled;
...
}
/**
* 分析6
* ViewGroup
* 使用头插法将touch目标放在链表的开头
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
ViewGroup的dispatchTouchEvent过程比较复杂,但可以被划分为一下几个步骤:
- 接收到Down事件后,重置标志
- 判断是否可以拦截事件,如果可以尝试拦截 (若无重写拦截函数,最终结果仍是未拦截)
- 双重循环查找可处理事件的子视图,并调用其dispatchTouchEvent分配事件
2.5 View
View是以事件处理者的身份存在的,因此它没有ViewGroup拥有的事件分发以及拦截的能力,View更关心事件的消费。由于View没有拦截事件的能力,自然也就没有onInterceptTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent event) {
...
// 优先出发onTouch回调
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// onTouch没有消费事件,调用onTouchEvent
if (!result && onTouchEvent(event)/* 分析1 */) {
result = true;
}
...
return result;
}
/**
* 分析1
* View
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED
&& (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
// 设置视图的按下状态,由于此处视图被禁用,因此设置为非按下状态
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// 可以点击但被禁用的视图仍然会消费事件,但不会响应事件。
return clickable;
}
// 委托处理触点落在此视图中但应由另一个视图处理的触摸事件。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 可以点击 或者 长按、悬停有提示 时
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
// 对不同事件类型进行区别处理
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!clickable) {
...
// 如果长按的回调仍未触发,则移除
removeLongPressCallback();
...
break;
}
...
if (prepressed) {
// 在我们实际显示按钮为按下之前,该按钮正在被释放。
// 为了确保用户看到它,使其变成按下的状态。
setPressed(true, x, y);
}
...
// 使用一个 Runnable 而不是直接调用 performClick。
// 这使得视图的其他视觉状态在单击操作开始之前可以继续更新。
if (mPerformClick == null) {
// PerformClick通过实现了Runnabl接口,使得可以直接post给主线程Handler处理
// 可以运行时,会通过UI线程来执行点击事件 分析1
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
// 若没有将 mPerformClick 添加到消息队列中则立即执行
// 执行点击事件的地方 分析2
performClickInternal();
}
...
break;
case MotionEvent.ACTION_DOWN:
...
mHasPerformedLongPress = false;
// 如果不能点击,尝试触发长按
if (!clickable) {
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
...
// 设置成按下状态,尝试触发长按
setPressed(true, x, y);
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
case MotionEvent.ACTION_CANCEL:
// 如果是可点击的,可能之前已经进入按下状态,因此需要取消状态
if (clickable) {
setPressed(false);
}
...
// 取消长按回调
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
...
// 获取手势分类
// CLASSIFICATION_NONE : 没有明显意图
// CLASSIFICATION_AMBIGUOUS_GESTURE :用户对当前事件流的意图尚未确定。
// 在分类解析为另一个值或事件流结束之前,应禁止手势操作(如滚动)。
// CLASSIFICATION_DEEP_PRESS :用户有意用力按屏幕。应使用此分类类型来加速长按行为
final int motionClassification = event.getClassification();
final boolean ambiguousGesture =
motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
...
if (ambiguousGesture && hasPendingLongPressCallback()) {
if (!pointInView(x, y, touchSlop)/* 事件移出了视图 */) {
// 此处的默认操作是取消长按。但在这里仅仅是延长超时,以防分类不明确。
removeLongPressCallback();
long delay = (long) (ViewConfiguration.getLongPressTimeout()
* mAmbiguousGestureMultiplier);
// 减去已经花费的时间
delay -= event.getEventTime() - event.getDownTime();
checkForLongClick(
delay,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
...
}
// 事件移出了视图
if (!pointInView(x, y, touchSlop)) {
...
// 移除长按回调
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
...
}
final boolean deepPress =
motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
if (deepPress && hasPendingLongPressCallback()) {
// 由于意图是CLASSIFICATION_DEEP_PRESS,立即执行长按相关的行为
removeLongPressCallback();
checkForLongClick(
0 /* 立即执行 */,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}
break;
}
return true;
}
return false;
}
/**
* 分析1
* View
*/
private final class PerformClick implements Runnable {
@Override
public void run() {
...
// 执行点击事件的地方 分析2
performClickInternal();
}
}
/**
* 分析2
* View
*/
private boolean performClickInternal() {
...
// 真正执行点击事件的地方 分析3
return performClick();
}
/**
* 分析3
* View
*/
public boolean performClick() {
...
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
// 播放点击音效
playSoundEffect(SoundEffectConstants.CLICK);
// 调用 onClick 事件
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
...
return result;
}
View的onTouchEvent由于要处理不同类型的事件因此显得复杂一些,我们对View的事件消费做一个简单的总结:
- dispatchTouchEvent 会优先触发 onTouch 回调,若没有设置onTouch监听则会触发onTouchEvent。
- ACTION_UP 会触发 onClick 回调。
- ACTION_DOWN 会尝试触发长按,在ACTION_MOVE移出视图或者其他事件中会取消这个回调。如果到时间还没取消则会执行长按的回调。
- 如果view的onTouchEvent返回false即事件没有被消费,则dispatchTouchEvent也会返回false。这将会导致父视图再次触发dispatchTransformedTouchEvent,但此时child是null,将执行super.dispatchTouchEvent,即将ViewGroup自身当成一个View来消费事件。