本质

实现方式为 递归 + 责任链
**
UI 层的事件分发机制指从 ViewRootImpl$ViewPostImeInputStage 开始到 DecorView,再到整个视图树的其它节点。

模型

MotionEvent —— 事件序列

每个事件都对视图树进行遍历是耗性能对,因此在 MotionEvent 中使用 Action 来描述该事件的行为。一个事件序列以 ACTION_DOWN 开始,经历若干个 ACTION_MOVE ,最终以 ACTION_UP 结束。

父节点开始不拦截 ACTION_DOWN 事件,然后在 ACTION_MOVE 的时候,进行拦截,此时 子节点的 dispatchTouchEvent 会收到 ACTION_CANCEL 事件。

TouchTarget —— ViewGroup#mFirstTouchTarget

链表结构,用于保存 ViewGroup 下一级事件的分发目标。责任链模式的标准实现方式,使用链表保存处理器。

拦截

父节点拦截

onInterceptTouchEvent 方法

ViewGroup 中的方法,父节点是否要拦截事件序列

子节点不允许拦截

requestDisallowInterceptTouchEvent方法

子节点调用 getParent.requestDisallowInterceptTouchEvent 来阻止父节点的拦截,内部实现是添加 flag 标志位,但如果该事件序列在最开始便被父节点拦截(这意味子节点没机会调用该方法),则子节点没机会拦截了

实现

ViewRootImpl 到普通 ViewGroup/View 的流程

UI 层的事件分从 ViewRootImpl$ViewPostImeInputStage 开始:
carbon.png
上图中的 mView 为 ViewRootImpl 中的属性,它代表着 DecorView 对象,即整个视图树的根。ViewRootImpl 与 DecorView 的关系可以参考 View 绘制:
🎨 View 绘制
上述流程会调用到 DecorView#dispatchTouchEvent 方法中,由于 DecorView 重写了 dispatchTouchEvent 方法,因此该过程与普通的 View 的事件分发操作略有不同(可以认为 DecorView 为自定义 View,因为 DecorView 是 FrameLayout 的子类)。

DecorView 自定义后的流程为,如果 DecorView#mWindow 的 getCallback() 的返回值不为 null 且符合条件,则调用 callBack 的 dispatchTouchEvent 方法,否则调用 super:
image.png
而此处的 Window.CallBack 的实现类为 Activity 和 Dialog,后续的逻辑是这样:

  1. public class DecorView extends FrameLayout {
  2. // 1.将事件分发给Activity
  3. @Override
  4. public boolean dispatchTouchEvent(MotionEvent ev) {
  5. return window.getActivity().dispatchTouchEvent(ev)
  6. }
  7. // 4.执行ViewGroup 的 dispatchTouchEvent
  8. public boolean superDispatchTouchEvent(MotionEvent event) {
  9. return super.dispatchTouchEvent(event);
  10. }
  11. }
  12. // 2.将事件分发给Window
  13. public class Activity {
  14. public boolean dispatchTouchEvent(MotionEvent ev) {
  15. return getWindow().superDispatchTouchEvent(ev);
  16. }
  17. }
  18. // 3.将事件再次分发给DecorView
  19. public class PhoneWindow extends Window {
  20. @Override
  21. public boolean superDispatchTouchEvent(MotionEvent event) {
  22. return mDecor.superDispatchTouchEvent(event);
  23. }
  24. }

上面伪代码摘自 却把青梅嗅:反思|Android 事件分发机制的设计与实现

这样做的目的是允许开发者通过重写 Activity/Dialog.onTouchEvent()方法 在最后的归流程进行一些逻辑处理

后续的流程便来到了 ViewGroup/View:

ViewGroup 流程

boolean dispatchTouchEvent(MotionEvent ev)

  1. 如果是第一次按下,清空 touchTarget 和之前的触摸状态

  2. 判断父节点是否拦截(为 intercepted 赋值)

    • actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null

该分支意味着可能是新的事件序列,或者当前序列已经被子节点处理过

  1. - 子节点允许拦截(`!disallowIntercept`

intercepted = onInterceptTouchEvent(ev); // 判断父节点是否要拦截

  1. - 子节点不允许拦截

intercepted = false;

  • else

走这个分支代表当前不是一个新的事件序列且子节点没能力处理,则 父节点拦截,因为即使发给子节点 也没能力处理

intercepted = true;

  1. 判断取消(为 canceled 赋值)
    • canceled **= resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;**

mPrivateFlags 中是否有 PFLAG_CANCEL_NEXT_UP_EVENT 或是 ACTION_CANCEL

  1. 既没取消,也没拦截(!canceled && !intercepted

    • for 循环当前子节点
      • 如果子节点 不能接受事件(不可见或正在执行动画)或 不在点击区域内,跳过
      • newTouchTarget = getTouchTarget(child) ,根据 child 寻找其对应的 touchTarget
      • if (dispatchTransformedTouchEvent)
        • 证明该 child 下有能力处理该事件,为 mFirstTouchTarget 赋值
  2. 分发给 touchTarget(为 handled 赋值)

    • mFirstTouchTarget == null
      • handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS)

child 参数传 null,意味着内部调用 super.onDispatchTouchEvent

  • else

分发给 mFirstTouchTarget

  1. - **alreadyDispatchedToNewTouchTarget && target == newTouchTarget**

如果已经分发过了则过滤掉 handled = true;

  1. - **else **
  2. - cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
  3. - if (**dispatchTransformedTouchEvent**(ev, cancelChild, **target**.child, **target**.pointerIdBits))

handled = true;

  1. return handled

boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)

image.png

View 流程

boolean dispatchTouchEvent(MotionEvent ev)

image.png
如果设置了 onTouchListener优先执行 onTouchListener.onTouch 方法 如果 onTouch 返回 false,则 result 仍为 false 此时会走底部分支,onTouchEvent 则有机会执行,后续执行 onClickListener