Android 事件分发介绍

一、目的

        最开始接触Android时,仅仅是知道Android系统存在的点击事件、触摸事件,但是并不清楚这些事件的由来。
        之后,在面试Oppo和美图时,皆有问到Android的事件分发机制,但是都被问得很懵逼,归根到底都是对于其实现逻辑的不理解。
        随后,想去弥补该模块的不足,浏览很多关于Android事件分发的博文,但仍存在一些疑惑,就想着去阅读下源码,整理下笔记,希望对同学们有帮助。

二、环境

  1. 版本:Android 11
  2. 平台:展锐 SPRD8541E

三、相关概念

3.1 事件分发

        Android 中 View 的布局是一个树形结构,各个 ViewGroup 和 View 是按树形结构嵌套布局的,从而会出现用户触摸的位置坐标可能会落在多个 View 的范围内,这样就不知道哪个 View 来响应这个事件,为了解决这一问题,就出现了事件分发机制。

四、详细设计

4.1应用布局

4.1.1 应用布局结构

        如下为一个Activity打开后,其对应视图的层级结构。

4.1.2 LayoutInspector

        Layout Inspector是google提供给我们进行布局分析的一个工具,也是目前google在弃用Hierarchy View后推荐使用的一款布局分析工具。

4.2 关键View&方法

4.2.1 相关View

组件描述
ActivityAndroid事件分发的起始端,其为一个window窗口,内部持有Decorder视图,该视图为当前窗体的根节点,同时,它也是一个ViewGroup容器。
ViewGroupAndroid中ViewGroup是一个布局容器,可以嵌套多个 ViewGroup 和 View,事件传递和拦截都由 ViewGroup 完成。
View事件传递的最末端,要么消费事件,要么不消费把事件传递给父容器

4.2.2 相关方法

方法描述
dispatchTouchEvent分发事件
onInterceptTouchEvent拦截事件
onTouchEvent触摸事件

4.2.3 View与方法关系

组件dispatchTouchEventonInterceptTouchEventonTouchEvent
Activity
ViewGroup
View

4.3 事件分发概念图

4.3.1 事件分发类图

4.3.2 事件分发模型图

        Android的ACTION_DOWN事件分发如图,从1-9步骤,描述一个down事件的分发过程,如果大家能懂,就不用看下面文字描述了(写完这个篇幅,感觉文字好多,不好理解!)

  1. ACTION_DOWN事件触发。 当我们手指触摸屏幕,tp驱动会响应中断,通过ims输入系统,将down事件的相关信息发送到当前的窗口,即当前的Activity。
  2. Activity事件分发。 会引用dispatchTouchEvent()方法,对down事件分发。Activity本身会持有一个window对象,window对象的实现类PhoneWindow会持有一个DecorView对象,DecorView是一个ViewGroup对象,即我们可以理解为,Activity最终会将事件分发给下一个节点——ViewGroup。
  3. ViewGroup事件拦截。 ViewGroup接收到事件后,会先引用onInterceptTouchEvent(),查看当前的视图容器是否做事件拦截。
  4. ViewGroup消费事件。 如当前的ViewGroup对事件进行拦截,即会调用onTouchEvent(),对事件消费。
  5. ViewGroup事件不拦截。 则ViewGroup会继续遍历自身的子节点,并且当事件的坐标位于子节点上,则继续下发到下一个节点。ViewGroup的子节点有可能是View,也可能是ViewGroup(当然,ViewGroup最后也是继承于View的,突然感觉有点废话)。
  6. ViewGroup事件分发。 目标视图如果是ViewGroup,会引用其super类的dispatchTouchEvent()方法,即事件下发,不管目标视图是View或者ViewGroup最终引用的是View类的分发方法。
  7. View事件消费。 在View的dispatchTouchEvent()方法中会根据当前View是否可以点击、onTouch()是否消费、onTouchEvent()是否消费等条件,来判断当前是否为目标View。
  8. View事件未消费。 View事件未消费,则其父节点,即ViewGroup会调用onTouchEvent()方法,并根据返回值来决定是否消费事件。
  9. ViewGroup事件未消费。 ViewGroup事件未消费,择其父节点,即Actviity会调用onTouchEvent()方法

PS:
(1) ACTION_MOVEACTION_UP事件,流程与ACTION_DOWN的分发过程基本一致,MOVE和UP事件也是通过Activity开始,借助DOWN事件产生的目标View,逐级分发。
(2) ACTION_CANCEL事件,是在down与up、move事件切换过程中,事件被拦截,两次的touchTarget目标view不一致,而产生的事件。用于对之前的目标View做恢复处理,避免down与up/move事件不对称。

4.4 Activity组件

4.4.1 Activity->dispatchTouchEvent()

        底层上报的事件信息,最终会引用到该方法。Activity会持有一个根视图DecordView,事件最终会往该ViewGroup分发,如所有的View都未消费该事件,则最终由Activity的onTouchEvent()
来兜底处理。

@frameworks\base\core\java\android\app\Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
 ...
 if (getWindow().superDispatchTouchEvent(ev)) {//Step 1. 查看Window对应的View是否分发该事件
 return true;
 }
 return onTouchEvent(ev);//Step 2. 如果没有组件消费事件,则由Activity兜底处理
}

4.4.2 Activity->getWindow()

        我们每次启动一个Activity的组件,会先打开一个window窗口,而PhoneWindow是Window唯一的实现类。

@frameworks\base\core\java\android\app\Activity.java
public Window getWindow() {
 return mWindow;
}
final void attach(Context context, ActivityThread aThread...) {
 ...
 mWindow = new PhoneWindow(this, window, activityConfigCallback);//PhoneWindow是Window窗口唯一的实现类
 ...
}

        PhoneWindow对象内部持有DecorView对象,而该View正是该窗口对应的视图容器,也是根节点。(此部分不具体分析)

@frameworks\base\core\java\com\android\internal\policy\PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder. Callback {
 ...
 private DecorView mDecor;//
 ...
 @Override
 public boolean superDispatchTouchEvent(MotionEvent event) {
 return mDecor.superDispatchTouchEvent(event);//往View的根节点分发事件
 }
}

4.4.3 Activity->onTouchEvent()

        Activity的onTouchEvent方法,是在没有任何组件消费事件的情况下,触发的方法。

@frameworks\base\core\java\android\app\Activity.java
public boolean onTouchEvent(MotionEvent event) {
 if (mWindow.shouldCloseOnTouch(this, event)) {
 finish();
 return true;
 }
 return false;
}

4.5 ViewGroup组件

        ViewGroup组件在整个事件分发的模型中,既有分发事件的责任,又要具备处理事件的能力,真的典型的当爹又当妈。
        当Activity调用superDispatchTouchEvent,即最终会使用到DecorView的superDispatchTouchEvent方法,而DecorView是继承于ViewGroup,即最终会引用ViewGroup的dispatchTouchEvent方法。

4.5.1 ViewGroup->dispatchTouchEvent()

此方法为事件分发最核心的代码。其主要处理如下四件事情:
Setp 1. 重置事件。 一次完整触摸的事件:DOWN -> MOVE -> UP,即我们可以理解为DOWN是所有触摸事件的起始事件。当输入事件是ACTION_DOWN时,重置触摸事件状态信息,避免产生干扰。
Step 2. 拦截事件。 拦截事件是ViewGroup特有的方法,用于拦截事件,并将该事件分发给自己消费,防止事件继续下发。
Step 3.查找目标View。 查找目标View主要针对于Down事件。当ViewGroup未拦截事件,且输入事件是ACTION_DOWN时,会遍历该ViewGroup的所有子节点,并根据触摸位置的坐标,来决定当前子节点是否是下一级目标View。当找到目标View节点后,会分发Down事件,并记录该节点信息。
Step 4.下发事件。 如果目标View未找到的话,则会将事件交由自己的onTouchEvent()处理;如果目标View已经找到,则Down事件就此结束(此处暂不考虑多指场景);Move和Up事件将继续下发(默认情况下Move、Up和Down事件是成对出现的,如果目标View已经存在,则Down事件已经下发,即意味着Move和Up事件也需要下发给对应的目标View)。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
 ...
 if (actionMasked == MotionEvent.ACTION_DOWN) {//Step 1.重置事件信息,避免影响下一次事件
 cancelAndClearTouchTargets(ev);
 resetTouchState();
 }
 if (actionMasked == MotionEvent.ACTION_DOWN
 || mFirstTouchTarget != null) {
 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
 if (!disallowIntercept) {
 intercepted = onInterceptTouchEvent(ev);//Step 2.拦截事件
 ev.setAction(action); // restore action in case it was changed
 }
 } 
 ...
 if (!canceled && !intercepted) {//Step 3.查找目标View
 if (actionMasked == MotionEvent.ACTION_DOWN
 || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
 || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
 ...
 if (newTouchTarget == null && childrenCount != 0) {
 ...
 for (int i = childrenCount - 1; i >= 0; i--) {//遍历所有的子节点
 ...
 if (!child.canReceivePointerEvents()
 || !isTransformedTouchPointInView(x, y, child, null)) {// 子节点不可以接收事件,或者触摸位置不在子节点的范围上
 continue;
 }
 ...
 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//找到目标View
 ...
 break;
 }
 }
 ...
 }
 ...
 }
 }
 //Step 4.根据找到的目标View情况,继续下发事件
 if (mFirstTouchTarget == null) {
 // No touch targets so treat this as an ordinary view.
 handled = dispatchTransformedTouchEvent(ev, canceled, null,
 TouchTarget.ALL_POINTER_IDS);//没有找到目标View或者事件被拦截,事件下发给自己
 } else {
 ...
 while (target != null) {//多组数据,一般是指多指场景
 final TouchTarget next = target.next;
 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {//此场景一般是down事件
 handled = true
 } else {
 if (dispatchTransformedTouchEvent(ev, cancelChild,
 target.child, target.pointerIdBits)) {//此场景一般是move、up事件
 handled = true;
 }
 ...
 }
 predecessor = target;
 target = next;
 }
 ...
 }
 ...
 return handled;
}

4.5.2 ViewGroup->dispatchTransformedTouchEvent()

事件分发关键方法,主要用于向目标View分发事件,具体逻辑如下:
Step 1.Cancel事件分发。 之前我们提过Down和Up事件是成对存在的,如果Down事件已经下发的情况下,Up事件却因为事件拦截等原因,未能下发给目标View,目标View未收到Up事件,此时就可能产生一些按压状态的异常问题,故,在当前场景下,将会分发一个ACTION_CANCEL事件给目标View。
Step 2.事件处理。 如果事件未找到目标View,则child会为null,此时的事件将由自身处理。
Step 3.事件分发。 如果事件还存在目标View,则此时的事件会再分发。

 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
 View child, int desiredPointerIdBits) {
 ...
 if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {//Step 1.下发取消事件
 event.setAction(MotionEvent.ACTION_CANCEL);
 if (child == null) {
 handled = super.dispatchTouchEvent(event);
 } else {
 handled = child.dispatchTouchEvent(event);
 }
 event.setAction(oldAction);
 return handled;
 }
 ...
 if (child == null) {//Step 2.如果事件未找到目标View,则触摸事件会发给自己
 handled = super.dispatchTouchEvent(transformedEvent);
 } else {
 final float offsetX = mScrollX - child.mLeft;
 final float offsetY = mScrollY - child.mTop;
 transformedEvent.offsetLocation(offsetX, offsetY);
 if (! child.hasIdentityMatrix()) {
 transformedEvent.transform(child.getInverseMatrix());
 }
 handled = child.dispatchTouchEvent(transformedEvent);//Step 3.找到目标View,事件下发给子节点
 }
 ...
 return handled;
 }

4.6 View组件

        View组件在事件处理模型中,主要是处理事件。我们知道ViewGroup,也是继承于View,所以ViewGroup也是同样具备View的处理事件能力。

4.6.1 View->dispatchTouchEvent()

Step 1.触发onTouch()方法。 如果当前的View是可点击的,且配置了onTouch事件监听,则触发该View的onTouch()方法。
Step 2.触发onTouchEvent()方法。 如果该事件在上一步的onTouch()函数中未被消费,则触发onTouchEvent()方法。

public boolean dispatchTouchEvent(MotionEvent event) {
 boolean result = false;
 ...
 if (onFilterTouchEventForSecurity(event)) {
 ...
 ListenerInfo li = mListenerInfo;
 if (li != null && li.mOnTouchListener != null
 && (mViewFlags & ENABLED_MASK) == ENABLED
 && li.mOnTouchListener.onTouch(this, event)) {//Step 1.触发onTouch事件
 result = true;
 }
 if (!result && onTouchEvent(event)) {//Step 2.如onTouch未消费,触发onTouchEvent事件
 result = true;
 }
 }
 ...
 return result;
}

4.6.2 OnTouchListener->onTouch()

        View可以设置事件监听,用于监听onTouch事件的回调,当然,像我们常见的onClick()、onLongClick()等事件也可监听,其相关源码如下:

@frameworks\base\core\java\android\view\View.java
public void setOnTouchListener(OnTouchListener l) {//设置onTouch监听
 getListenerInfo().mOnTouchListener = l;
}
ListenerInfo getListenerInfo() {
 if (mListenerInfo != null) {
 return mListenerInfo;
 }
 mListenerInfo = new ListenerInfo();
 return mListenerInfo;
}
public interface OnTouchListener {//Touch接口,用于回调onTouch事件
 boolean onTouch(View v, MotionEvent event);
}

4.6.3 View->onTouchEvent()

        事件如未被onTouch消费掉,则会引用到onTouchEvent()方法,该方法会涉及ACTION_UP、ACTION_DOWN、ACTION_CANCEL、ACTION_MOVE事件的处理,View的onClick()、onLongClick()也是由该方法触发。此外,如果当前的View是可点击的话,则直接消费该事件。

public boolean onTouchEvent(MotionEvent event) {
 ...
 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
 || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
 || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;//当前View是否可点击
 ...
 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
 switch (action) {
 case MotionEvent.ACTION_UP://抬起
 ...
 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
 if (!focusTaken) {
 removeLongPressCallback();//若有长按事件未处理,则移除长按事件
 if (mPerformClick == null) {
 mPerformClick = new PerformClick();
 }
 if (!post(mPerformClick)) {//通过Hanlder将点击事件发送到主线程执行
 performClickInternal();//如果不成功,则直接引用点击事件
 }
 }
 }
 if (mUnsetPressedState == null) {
 mUnsetPressedState = new UnsetPressedState();//更新按钮的按压事件
 }
 ...
 break;
 case MotionEvent.ACTION_DOWN://按下
 ...
 if (isInScrollingContainer) {//在可滚动的容器内,为了容错,延迟点击
 ...
 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
 } else {
 setPressed(true, x, y);//设置按下的状态
 checkForLongClick(
 ViewConfiguration.getLongPressTimeout(),
 x,
 y,
 TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);//开启一个长按延时事件
 }
 break;
 case MotionEvent.ACTION_CANCEL://取消
 ...
 break;
 case MotionEvent.ACTION_MOVE://移动
 ...
 break;
 }
 return true;//如果是可点击的View,即消费事件
 }
 ...
 return false;
}

4.7 例子-点击事件时序图

        如下是Android的点击事件时序图,如果能够理解单击事件的由来,对于整个事件分发的知识要点已大体掌握。

五、小结&问题点

  1. 事件分发流程?包括ACTION_DWON、ACTION_UP、ACTION_MOVE事件的处理过程;
  2. ACTION_CANCEL事件的使用场景?父控件对move事件拦截场景?
  3. 单击、长按、触摸事件的产生过程?
  4. 点击一个View未抬起,同时move该事件直至离开当前View的范围,处理过程如何?
  5. 如果所有View都未消费事件,流程如何?
  6. ViewPage+ListView,左右滑动和上下滑动冲突的解决问题?即事件拦截过程?
  7. 普通的View是根据什么来决定是否消费事件,例如Button?
    =>答:如无重写onTouchEvent事件,根据当前的View是否可点击,来决定是否消费事件。

        我最开始没有看源码,直接去看博客上的内容,弯弯绕绕,似懂非懂。在面试的过程中,面试官举个场景分析流程,我都懵逼,分析不出来,现场很尴尬。之后看源码,整体流程代码量很少,感叹于Android事件分发流程的设计,很少的代码量,却承载了很重要的功能,而没有见过该模块发生过异常。
        多读书,多看报,少吃零食,多睡觉!

六、代码仓库地址

Demo地址:  https://gitee.com/linzhiqin/custom-demo

七、参考资料

https://zhuanlan.zhihu.com/p/623664769?utm_id=0
事件分发视频(总结很好,但是得先理解基本概念,才方便学习)
https://www.bilibili.com/video/BV1sy4y1W7az?p=1&vd_source=f222e3bf3083cad8d9f660629bc47c16

作者:林奋斗同学原文地址:https://www.cnblogs.com/zhiqinlin/p/17957738

%s 个评论

要回复文章请先登录注册