Skip to content

View事件分发机制

zhpanvip edited this page Mar 17, 2021 · 23 revisions

View事件分发

1.事件分发机制流程

Android系统中的输入事件为InputEvent,InputEvent又分为KeyEvent和MotionEvent。KeyEvent对应着键盘输入事件,而MotionEvent则对应着屏幕触摸事件。这两种事件都由InputManager统一分发。

在系统启动时,SystemServer会启动窗口管理服务WindowManagerService,在WindowManagerService中会起动输入管理器InputManager来负责监控键盘消息。 InputManager负责从硬件接受输入事件,并将事件分发给当前激活的窗口处理,而InputManager与Window之间的通信是通过ViewRootImpl类实现的。

ActivityThread负责启动Activity,在performaLaunchActivity流程中,ActivityThread会为Activity创建PhoneWindow和DocerView,然后在handleResumeActivity()中会将PhoneWindow和InputManagerService建立起链接,保证UI可见时能够对输入事件进行正确的分发。

当InputManager监控到硬件层的输入事件时,会通知ViewRootImpl对事件进行底层分发。

在最新的 Android 系统中,事件是由InputStage来分别处理,它们都有一个回调接口 onProcess 函数,这些都声明在 ViewRootImpl 内部类里面,并且在 setView 里面进行注册,比如有 ViewPreImeInputStage 用于分发 KeyEvent.

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                // ...
                InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
                InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
                        "aq:native-post-ime:" + counterSuffix);
                InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
                InputStage imeStage = new ImeInputStage(earlyPostImeStage,
                        "aq:ime:" + counterSuffix);
                InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
                InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
                        "aq:native-pre-ime:" + counterSuffix);
          }
     }
}

这里我们重点关注与 MotionEvent 事件分发相关的 ViewPostImeInputStage。在它的 onProcess 函数中,如果判断事件类型是 SOURCE_CLASS_POINTER,即触摸屏的 MotionEvent 事件,就会调用 mView 的 dispatchPointerEvent 方法处理。

UI层级的事件分发 作为 完整事件分发流程的一部分,发生在ViewPostImeInputStage.processPointerEvent

// ViewRootImpl
final class ViewPostImeInputStage extends InputStage {

   @Override
   protected int onProcess(QueuedInputEvent q) {
       if (q.mEvent instanceof KeyEvent) {
           return processKeyEvent(q);
       } else {
           final int source = q.mEvent.getSource();
           if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
               return processPointerEvent(q);
           } 
       }
   }

  private int processPointerEvent(QueuedInputEvent q) {
    // 让顶层的View开始事件分发
    final MotionEvent event = (MotionEvent)q.mEvent;
    boolean handled = mView.dispatchPointerEvent(event);
    //...
  }
}

也就是说事件分发最开始是传递给 DecorView 的,DecorView 的 dispatchTouchEvent 是传给 Window Callback 接口方法 dispatchTouchEvent,而 Activity 实现了 Window Callback 接口,在 Activity 的 dispatchTouchEvent 方法里,是调到 Window 的 dispatchTouchEvent,Window 的唯一实现类 PhoneWindow 又会把这个事件回传给 DecorView,DecorView 在它的 superDispatchTouchEvent 把事件转交给了 ViewGroup。

事件分发的流程是:

DecorView -> Activity -> PhoneWindow -> DecorView -> ViewGroup -> View

(1)事件分发的流程

事件分发的流程涉及了三个元素,Activity、ViewGroup 和 View。Activity 的 dispatchTouchEvent 前面说过,它的 dispatchTouchEvent 一般都是返回 false 不消费往下传;在说 View 的 dispatchTouchEvent,如果注册了 OnTouchListener 就调用其 onTouch 方法,如果 onTouch 返回 false 还会接着调用 onTouchEvent 函数,onTouchEvent 作为一种兜底方案,它在内部会根据 MotionEvent 的不同类型做相应处理,比如是 ACTION_UP 就需要执行 performClick 函数。ViewGroup 因为涉及对子 View 的处理,其派发流程没有 View 那么简单直接,它重写了 dispatchTouchEvent 方法,如果 ViewGroup 允许拦截,就调用其 onInterceptTouchEvent 来判断是否要真正执行拦截了,如果拦截了就交由自己的 onTouchEvent 处理,如果不拦截,就从后遍历子 View 处理,它有两个函数可以过滤子 View,一个是判断这个子 View 是否接受 Pointer Events 事件,另一个是判断落点有没有落在子 View 范围内。如果都满足,则调用其 dispatchTouchEvent 处理。如果该子 View 是一个 ViewGroup 就继续调用其 dispatchTouchEvent,否则就是 View 的 dispatchTouchEvent 方法,如此循环往复,直到事件真正被处理。

其伪代码如下:

// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean consume = false;
    if (onInterceptTouchEvent(event)) {
        consume = onTouchEvent(event);
    } else {
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}

构建事件序列分发链

在事件分发过程中,如果都对View进行一次递归遍历,是一个很耗性能的过程,因此对其进行了优化。当接收到ACTIVITY_DOWN事件时,以为着一个完整序列的开始。通过递归遍历找到View中真正对事件进行消费的View,并将其保存。之后接收到ACTIVITY_MOVE和ACTIVITY_UP事件时则跳过递归过程,将事件直接分发给Child。

在ViewGroup中有一个mFirstTouchTarget的成员变量,用来保存消费事件的View,代码如下:

public abstract class ViewGroup extends View {
    // 指向下一级事件分发的`View`
    private TouchTarget mFirstTouchTarget;

    private static final class TouchTarget {
        public View child;
        public TouchTarget next;
    }
}

每个ViewGroup都持有一个mFirstTouchTarget, 当接收到一个ACTION_DOWN时,通过递归遍历找到View树中真正对事件进行消费的Child,并保存在mFirstTouchTarget属性中,依此类推组成一个完整的分发链。

在这里插入图片描述

在这之后,当接收到同一事件序列的其它事件如ACTION_MOVE、ACTION_UP时,则会跳过递归流程,将事件直接分发给 分发链 下一级的Child:

// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
  boolean consume = false;
  // ...
  if (event.isActionDown()) {
    // 1.第一次接收到Down事件,递归寻找分发链的下一级,即消费该事件的View
    // 这里可以看到,递归深度搜索的算法只执行了一次
    mFirstTouchTarget = findConsumeChild(this);
  }

  // ...
  if (mFirstTouchTarget == null) {
    // 2.分发链下一级为空,说明没有子`View`消费该事件
    consume = super.dispatchTouchEvent(event);
  } else {
    // 3.mFirstTouchTarget不为空,必然有消费该事件的`View`,直接将事件分发给下一级
    consume = mFirstTouchTarget.child.dispatchTouchEvent(event);
  }
  // ...
  return consume;
}

事件拦截机制

ViewGroup中通过onInterceptTouchEvent来决定是否拦截该事件,

// 伪代码实现
// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
  // 1.若需要对事件进行拦截,直接中止事件向下分发,让自身决定是否消费事件,并将结果返回
  if (onInterceptTouchEvent(event)) {
    return super.dispatchInputEvent(event);
  }
  // ...
  // 2.若不拦截当前事件,开始事件分发流程
}

为了避免额外的开销,设计者根据 事件序列 为 事件拦截机制 做出了额外的优化处理,保证了 事件拦截的判断在一个事件序列中只处理一次,伪代码简单实现如下:

// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
  if (mFirstTouchTarget != null) {
    // 1.若需要对事件进行拦截,直接中止事件向下分发,让自身决定是否消费事件,并将结果返回
    if (onInterceptTouchEvent(event)) {
      // 2.确定对该事件序列拦截后,因此就没有了下一级要分发的Child
      mFirstTouchTarget = null;
      // 下一个事件传递过来时,最外层的if判断就会为false,不会再重复执行onInterceptTouchEvent()了
      return super.dispatchInputEvent(event);
    }
  }

  // ...
  // 3.若不拦截当前事件,开始事件分发流程
}

2.ViewGroup中的mFirstTouchTarget是一个什么东西,它有什么作用?

在ViewGroup中有一个类型为TouchTrarget的mFirstTouchTarget的成员变量,它是用来保存消费事件的子View的信息的。代码如下:

private static final class TouchTarget {
        @UnsupportedAppUsage
        public View child;
        public TouchTarget next;
       // ...省略无关代码
    }

可以看到TouchTarget内部保存了一个View和一个类型为TouchTarget的next成员变量,也就是说TouchTarget是一个链表结构。为什么是链表结构呢?主要是因为Android系统是支持多点触控的,所以TouchTarget设计成了链表。

设计mFirstTouchTarget的目的是为了避免在所有的事件序列中都去递归查找要消费事件的View,只需要在ACTION_DOWN中递归查找消费的View,并将View封装后赋值为mFirstTouchTarget,避免了后续一系列事件的查找。

mFirstTouchTarget会在ACTION_DOWN的时候被赋值,查找是否有能够消费事件的子View,如果有则将这个View包装成TouchTarget赋值给mFirstTouchTarget,否则mFirstTouchTarget为null。

接下来的一系列ACTION_MOVE事件都会根据mFirstTouchTarget是否为null和onInterceptTouchEvent来判断是否要拦截事件。所以mFirstTouchTarget在事件分发的流程中占了非常重要的作用。

3.如果在ViewGroup中拦截了ACTION_DOWN事件会怎样?

首先来看ViewGroup的dispatchTouchEvent方法的伪代码:

// ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
             // 调用自身的onInterceptTouchEvent方法来判断是否拦截事件
            intercepted = onInterceptTouchEvent(ev);
        } else {
            intercepted = true;
        }
        // 如果在ACTION_DOWN中拦截了事件,那么intercepted恒为true,则无法给mFirstTouchTarget赋值
        if (!canceled && !intercepted) {
            mFirstTouchTarget = findConsumeChild(this);
        }
        if (mFirstTouchTarget == null) {  
        	// 则调用自身的dispatchTouchEvent方法分发事件             
            handled = super.dispatchTouchEvent(event);
        } else{
        	// 则调用子View的dispatchTouchEvent方法
			handled = mFirstTouchTarget.child.dispatchTouchEvent(event);
		}
        return handled;
    }

从上面的代码中可以看到,如果在onInterceptTouchEvent方法中拦截ACTION_DOWN事件,则intercepted为true,而intercepted为true直接导致了mFirstTouchTarget无法被赋值。

接下来,一系列ACTION_MOVE以及ACTION_UP等事件都无法再调用onInterceptTouchEvent方法,也就是intercepted恒为true,且mFirstTouchTarget恒为null。

再往下,由于mFirstTouchTarget恒为null,就导致了所有的Motion事件(也包括ACTION_DOWN事件)只能交由自身处理,无法再将事件分发给子View。

这一点在事件分发流程中非常重要,通过Down事件确定了要处理该事件的View,接下来所有的Move事件序列就不会再走递归,而是直接交给这个View来处理。

4.为什么设置了onTouchListener后onClickListener不会被调用?

// View
public boolean dispatchTouchEvent(MotionEvent event) {
            //noinspection SimplifiableIfStatement
            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中如果li.mOnTouchListener不为null,则调用li.mOnTouchListener.onTouch,而如果li.mOnTouchListener.onTouch返回了true,则下边的onTouchEvent就不会被调用,而onClickListener就是在onTouchEvent方法中才被调用的。

// 伪代码
public boolean onTouchEvent(MotionEvent event) {
	public boolean onTouchEvent(MotionEvent event) {
		case MotionEvent.ACTION_UP:
			li.mOnClickListener.onClick(this);
		break;
	}
}

5.为什么一个View设置了setOnTouchListener会有提示没有引用performClick方法的警告?

当你添加了一些点击操作,例如像setOnClickListener这样的,它会调用performClick才可以完成onClick方法的调用,但你重写了onTouch,就有可能使得performClick没有被调用,这样这些点击操作就没办法完成了,所以就会有了这个警告。

参考

一文读懂Android View事件分发机制

反思|Android 事件分发机制的设计与实现

反思|Android 事件拦截机制的设计与实现

图解 Android 事件分发机制

公众号:玩转安卓Dev

Java基础

面向对象与Java基础知识

Java集合框架

JVM

多线程与并发

设计模式

Kotlin

Android

Android基础知识

Android消息机制

Framework

View事件分发机制

Android屏幕刷新机制

View的绘制流程

Activity启动

性能优化

Jetpack&系统View

第三方框架实现原理

计算机网络

算法

其它

Clone this wiki locally