文章目录
☞ 阅读原文
情境(Situation)
1. 专注于移动互联网数年,作为高P的我【鼓掌】竟然对事件分发机制见招拆招,似懂非懂。不专业,没法忍。
2. View树的递归嵌套逻辑让广大一线同行云里雾里,手足无措。
冲突(Complication)
1. 网上好多相关主题的博客,描述信息点非常多(但是ACTION_CANCEL描述很少),看完后不明觉厉。
2. 事件分发主要用于解决自定义炫酷控件以及滑动嵌套引发的冲突问题(程序傻傻分不清是横滑还是竖滑),发现同行各种写法都有,雷无处不在【人在家中坐,锅从天上来】。

疑问(Question)
1. 有没有体系化剖析套路?
2. 指出常见错误,给出最佳实践?
3. 清晰明了的给出一张图,便于查阅?
4. “鱼”和“渔”可以兼得?
答案(Answer)
剖析
论点
约法三章
1. 限于个人水平,本文只包含单点触控事件(ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL)。
2. Window类相关的我不会,肤浅的认为和事件分发关系不大(求大牛点拨),直接跳过。
3. 一家之言,姑妄言之,姑妄听之。
点
1. 事件流一致性保证(Consistency Guarantees):按下开始,中间可能伴随着移动,松开或者取消结束。ACTION_DOWN -> ACTION_MOVE(*) -> ACTION_UP/ACTION_CANCEL。
2. View类的dispatchTouchEvent方法完成事件的消费处理,ViewGroup的dispatchTouchEvent方法完成事件的分发处理。正常情况下不建议重写该方法改变系统事件分发机制。
3. ViewGroup类的onInterceptTouchEvent方法完成事件的拦截处理。事件分发路径上的ViewGroup,在ACTION_DOWN或者不是自己直接消费事件时一定会调用onInterceptTouchEvent方法。
4. View类的onTouchEvent方法完成具体处理事件消费,即触发点击监听(OnClickListener)和长时间点击监听(OnLongClickListener)以及按键状态、焦点相关处理。
1. 如果设置了OnTouchListener,会先调用OnTouchListener,如果该监听onTouch返回true,则不会调用onTouchEvent,直接返回已消费;
2. 如果设置了TouchDelegate ,onTouchEvent中会先调用TouchDelegate,如果该类onTouchEvent返回true,则直接返回已消费;
3. 如果View 可点击,执行onTouchEvent中事件处理,并返回true;
1. ACTION_DOWN:置按键标志位为按下状态,并触发延时(500ms)执行长按点击事件。
2. ACTION_MOVE:如果按键坐标超出该控件区域,则置按键标志位为非按下状态,并且移除ACTION_DOWN触发的延时执行长按点击事件。
3. ACTION_UP:如果按键标志位为按下状态,并且ACTION_DOWN触发的长按点击事件还未执行,则移除长按点击事件,执行点击事件。
4. ACTION_CANCEL:置按键标志位为非按下状态,移除ACTION_DOWN触发的延时执行长按点击事件。
4. 否则不可点击,返回false;
论据
基于 Android 8.0 (API Level 28) 源码解析
人机交互
赏析
用户的按键行为->手机传感器->ViewRootImpl->DecorView->WindowCallbackWrapper->Activity->PhoneWindow->DecorView->ViewGroup*->View->程序员的代码逻辑->硬件(显示器、扬声器等)响应输出->用户感知
View树
赏析
1. View是由树形结构组织,节点为ViewGroup或者View。ViewGroup可以包含多个子节点,View没有子节点。
2. Android中View树的根节点为DecorView(父View为FrameLayout,属于ViewGroup)。
3. Android中用户可自定义的View子树根节点id为“android:id/content”。
{:.info}
类图
赏析
1. ViewRootImpl是Android层逻辑起始点,用于接收来自系统底层的事件消息。相当于View管理类,本身不是View。(BTW:View绘制流程的三部曲(measure、layout、draw)也由该类触发的。)
2. DecorView是Android View树的根节点,持有window对象。本身能够直接进行真正事件分发能力(继承了父类ViewGroup和View的事件分发处理功能),但是事件分发会直接调用window,间接传递到Activity的事件分发,后续会由Activity回调DecorView的真正事件分发能力。对应图中的环形依赖。
3. Activity是Android中的页面,真正的事件分发由该类的dispatchTouchEvent触发。(Easter Eggs:如果你想让用户操作不了你的界面,蒙一层透明的View是不是有点low,直接重写该方法就可以控制。)
4. ViewGroup负责事件分发和拦截处理。按下事件和后续事件(移动、释放或者取消)处理不相同。
1. 按下事件,先判断是否拦截。
1. 如果不拦截的话,分发事件寻找目标消费子View(逆序遍历子View,递归调用子View的事件分发,判断是否有子View消费。mFirstTouchTarget存储目标消费子View对象)。
1. 如果有子View消费,则目标子View消费事件。
2. 否则自己尝试消费事件。
2. 否则直接自己尝试消费事件。
2. 后续事件
1. 如果按下事件找到了目标消费子View,则判断是否拦截,否则不拦截。
2. 如果有目标消费子View,则根据是否拦截。
1. 如果没有拦截,正常传送后续事件;
2. 如果有拦截,则当前事件转换为取消事件发送给目标消费子View,并且重置目标消费子View为空,接下来的后续事件直接自己尝试消费事件(不管是否消费,后续事件都会接收到&尝试处理事件分发);
3. 否则自己尝试消费事件。(不会调用是否拦截,其实拦截或者不拦截,都是自己消费事件。)
5. View负责事件消费事件处理。
1. 调用mOnTouchListener的onTouch。
1. 如果消费,直接返回true;
2. 否则,继续调用onTouchEvent方法;
1. 如果为启用的(enable),返回可点击(clickable)。
2. 否则,调用mTouchDelegate的onTouchEvent。
1. 如果消费,直接返回true;
2. 否则,
1. 如果可点击(clickable)
1. 进行事件流(ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL)处理(包含焦点、按键状态、按键和长时间按键);
2. 返回true。
2. 否则返回false;
注释
DecorView
/**
* Decor的意思是:装饰,布置。
* View树的根节点。
* 事件分发的启点,ViewRootImpl最先调用dispatchPointerEvent(实现在父类View里面)。
* 事件调用在DecorView里面形成了一个环。(先通过Window交由Activity分发,Activity再调用DecorView中的真正事件分发方法)
*/
public class DecorView extends FrameLayout {
private PhoneWindow mWindow;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// DecorView直接覆盖ViewGroup的事件分发实现,其实这只是饶了个圈,
// 正真的事件分发会由Activity回调到superDispatchTouchEvent(ViewGroup的事件分发处理)。
// 调用Window的WindowCallbackWrapper对象继续分发。
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
public boolean superDispatchTouchEvent(MotionEvent event) {
// 调用父类ViewGroup进行事件分发处理。
return super.dispatchTouchEvent(event);
}
}
WindowCallbackWrapper
/**
* Wrapper的意思是包装材料。
* 实实在在的一个壳,包裹着Activity。
*/
public class WindowCallbackWrapper implements Window.Callback {
final Window.Callback mWrapped;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// 交给Callback(具体对象为Activity)接力事件分发。
return mWrapped.dispatchTouchEvent(event);
}
}
Activity
/**
* Activity和View不一样,Activity就是一个壳,没有事件分发机制,View树如果没有消费,Activity捡个漏。
*/
public class Activity implements Window.Callback {
private Window mWindow;
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 交给Window(具体对象为PhoneWindow)接力事件分发。
if (getWindow().superDispatchTouchEvent(ev)) {
// View树消费掉事件
return true;
}
// 如果View树没有消费事件,Activity消费事件的机会来了。
// 启示:如果View树消费事件,在按下事件的后续事件中,如果父ViewGroup进行拦截,
// 虽然后续返回的消费状态对整个事件流没有影响,但是会对Activity有影响(View数不消费,Activity有机会消费)。
return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
// 事件消费处理,系统默认基本不干啥
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
}
PhoneWindow
/**
* PhoneWindow也是一个壳,将事件转回给DecorView分发处理。
*/
public class PhoneWindow extends Window {
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
// 交给DecorView接力事件分发(自此,环形结束,开始ViewGroup和View中事件分发和消费闪亮登场)。
return mDecor.superDispatchTouchEvent(event);
}
}
ViewGroup
/**
* ViewGroup,View容器的意思。
* dispatchTouchEvent完成时间分发逻辑。
* onInterceptTouchEvent:为事件拦截接口,父控件可以主动截留事件自己消费,否则只能等子Viwe树都不消费才能捡漏。【有控制权就是爸爸】
*/
public abstract class ViewGroup extends View implements ViewParent {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
// 按下事件会进行状态重置。(才有外部拦截法解决滑动冲突的小伙伴要注意这里重置,拦截调用必须要做此之后。)
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
// 是否拦截判断
final boolean intercepted;
// 拦截条件1,要么是按下事件,要么自己不直接消费事件。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 拦截条件2,允许拦截开关打开。
//(默认状态是打开的,其他View可以调用requestDisallowInterceptTouchEv