Skip to content

View事件分发机制

zhpanvip edited this page Mar 18, 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);
    //...
  }
}

View的dispatchPointerEvent中会调用dispatchTouchEvent,而在DecorView中重写了dispatchTouchEvent方法,代码如下:

// DecorView
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    final Window.Callback cb = mWindow.getCallback();
    return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
            ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

上述代码中的Callback即为Activity。也就是说事件分发最开始是传递给 DecorView 的,DecorView 的 dispatchTouchEvent 是传给 Window.Callback接口方法 dispatchTouchEvent,而 Activity 实现了 Window.Callback 接口。

紧接着Activity 的 dispatchTouchEvent方法里,是调到 Window 的 superDispatchTouchEvent,

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

Window 的唯一实现类 PhoneWindow 又会把这个事件回传给 DecorView,DecorView 在它的 superDispatchTouchEvent 把事件转交给了 ViewGroup。

    // PhoneWindow
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    // DecorView
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

至此,触摸事件正式进入ViewGroup中,开启事件的分发流程。总结一下事件分发的流程是:

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

不难看出,事件的传递过程是一个典型的责任链模式。

####(1)事件分发的流程

事件分发涉及到的三个分发
  • dispatchTouchEvent 事件从Activity经Window,最终会调用到DecorView(即ViewGroup)的dispatchTouchEvent。这个方法的作用是进行事件分发,只要事件能到达ViewGroup那么dispatchTouchEvent方法必定会被调用。dispatchTouchEvent方法返回一个boolean值,代表事件是否被消费。

  • onInterceptTouchEvent 该方法是ViewGroup独有的方法,在ViewGroup的dispatchTouchEvent中被调用,返回一个boolean值,用来判断当前ViewGroup是否要拦截事件。如果返回true,则表示该ViewGroup要拦截事件。事件交由ViewGrroup处理。

  • onTouchEvent onTouchEvent方法会在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跟ViewGroup内部一探究竟。

View中的dispatchTouchEvent

View中dispatchTouchEvent方法实现逻辑比较简单,简化后代码如下:

     public boolean dispatchTouchEvent(MotionEvent event) {

        //... 省略了对滑动等部分的处理逻辑代码
        boolean result = false;
        // 如果View是enable状态,并且设置了TouchListener,则调用TouchListener的onTouch,
        // 如果onTouchEvent方法返回true,则将true作为dispatchTouchEvent的返回值,表示事件
        // 被当前View消费掉了
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        // 如果View是disable状态或者没有设置TouchListener或者TouchListener的onTouch方法反回了false
        // 那么就调用View自身的onTouchEvent来处理事件,onTouchEvent返回true表示当前View消费了事件。
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    return result;
    }

View中的onTouchEvent方法会对事件进行兜底处理,比如在ACTION_UP中调用performClick方法,进而执行View的OnClick方法

public boolean onTouchEvent(MotionEvent event) {
     // 判断View是否是可以点击状态,即View设置了clickable或者longClickable或者contextClickable
     final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
         switch (action) {
           case MotionEvent.ACTION_UP:
           // 伪代码,这里省略了所有其他逻辑
           performClick();

           break;
        }
    }
}

public boolean performClick() {

    final boolean result;
    final ListenerInfo li = mListenerInfo;
    // 如果设置了OnClickListener,则调用OnClickListener的onClick方法
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    return result;
}
ViewGroup中的dispatchTouchEvent

首先由于ViewGroup是继承View的,那么它的dispatchTouchEvent有两条路可以走,

  • (1)可以调用super.dispatchTouchEvent

如果调用super.dispatchTouchEvent意味着调用了View的dispatchTouchEvent中的逻辑,即事件交由自身处理。

  • (2)调用自身的dispatchTouchEvent。

如果调用自身的dispatchTouchEvent,即走事件的分发流程,向下分发事件。

至于这两条路是如何选择的,就要详细分析ViewGroup中的dispatchTouchEvent方法了。下面贴一下dispatchTouchEvent简化后的代码:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // 如果是Down事件,事先置空mFirstTouchTarget
        cancelAndClearTouchTargets(ev);
        // 重置是否允许拦截的拦截标记
        resetTouchState();
    }
    final boolean intercepted;
    // 如果是DOWN事件,那么此时mFirstTouchTarget一定为null,且
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        // 1.如果是DOWN事件,那么一定能走到这里来,调用自身的onInterceptTouchEvent询问是否要对事件拦截,如果此时,
        //   我们拦截了事件拦截了事件会怎样呢?intercepted为true,那么下边FLAG位置的if语句一定不会执行,造成的结果是
        //   mFirstTouchTarget无法被赋值,即后续的MOVE事件序列mFirstTouchTarget恒为null。
        // 2.如果不是DOWN事件,能进到这里那么mFirstTouchTarget必定不为null,也说明该ViewGroup一定没有拦截DOWN事件
        intercepted = onInterceptTouchEvent(ev);
    } else {
        intercepted = true;
    } 

    // 打一个FLAG 
    // 注意此处的条件!!!!,此处的intercepted是理解事件分发的关键因素,根据上边分析的条件思考一下,
    // intercepted什么时候是false,什么时候是true? 可以来分情况讨论
    // (1)如果是DOWN事件,那么拦截了DOWN事件intercepted为true,否则为false
    // (2)如果非DOWN事件,mFirstTouchTarget不为null,那么拦截了事件则为true,否则为false
    // (3)如果不是DOWN事件,且mFirstTouchTarget为null,那么intercepted一定为true。
    //    此时会跳过此处的if语句,将事件交给自身处理。之所以会出现这种情况是因为前面拦截了DOWN事件。
    // 对上边的三种情况以此执行下边的代码,分析会有什么样的结果
    if (!intercepted) { 
        // 别管前面的条件如何,能走到这里来,意味着当前ViewGroup不拦截事件,即走上述的第二条路,向子View分发事件  
        // DFS遍历View树查找消费事件的View
        for (int i = childrenCount - 1; i >= 0; i--) {
          final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
          final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
          // 检查View是否能接收事件,即事件的坐标在子View内,并且View没有在执行动画
          if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {
               ev.setTargetAccessibilityFocus(false);
               continue;
         }

         // 代码能执行到此处说明已经查找到了符合条件的子View

         // 查找到目标View之后,通过dispatchTransformedTouchEvent开始向子View分发事件,
         // 即可以理解为调用child的dispatchTouchEvent方法,并将child的dispatchTouchEvent返回值返回
         // 那么dispatchTransformedTouchEvent的返回值就意味着子View是否消费了事件。
         if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
               // 走到这里说明子View中消费了事件,那么就将该View保存到mFirstTouchTarget中,
               // 该保存操作是在addTouchTarget方法中完成
               newTouchTarget = addTouchTarget(child, idBitsToAssign);
               alreadyDispatchedToNewTouchTarget = true;
               // 结束分发流程
               break;
         }
         // 代码能走到此处说明子View没有消费事件,那可以接着执行后边的代码了
       }
    } 
   // mFirstTouchTarget为null,说明没有子View消费事件
   if (mFirstTouchTarget == null) {
        // 调用dispatchTransformedTouchEvent方法,注意这里child的参数是null,
        // 即走上述中的第一条路,调用super.dispatchTouchEvent方法把事件交给自身View处理
        handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
   } else {  
        // 分析什么情况下能走到此处?该ViewGroup没有拦截DOWN事件,并且DOWN事件中遍历子View找到了消费事件的子View,
        // 那么此时会为mFirstTouchTarget赋值,并且结束DOWN事件的分发流程(注意,此时DOWN事件不会执行到此处的代码)
        // 接下来会有一系列的MOVE事件,如果此时没有拦截MOVE事件,那么MOVE事件就会跳过上边的if语句,即跳过事件分发的流程
        // 直接将MOVE事件分发给mFirstTouchTarget中的child,即跳过了遍历子View查找消费事件View的流程,节省了性能。
        TouchTarget target = mFirstTouchTarget;
        // 注意到此处是一个While循环,因为TouchTarget是一个链表,由于多指触控的支持,会形成了一个TouchTarget链表
        // 多个TouchTarget代表就有多个触控点,因此,此处会循环遍历链表,然后将多个触控点分发到child View.
        while (target != null) {
           final TouchTarget next = target.next;
           if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
               handled = true;
           } else {
              final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;
              // TouchTarget中的child成员才是真正消费事件的View
              if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
                  handled = true;
              }
              target = next;
          }
   }
}


private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        // dispatchTransformedTouchEvent的大致逻辑是这样的,这里简化了代码,理解就好。
        if (child == null) {
            // 调用自身的dispatchTouchEvent方法
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            // 调用子View的dispatchTouchEvent方法
            handled = child.dispatchTouchEvent(transformedEvent);
        }
}

总结一下,在View中,如果注册了 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 方法,如此循环往复,直到事件真正被处理。

构建事件序列分发链

在事件分发过程中,如果都对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