Android 事件分发介绍,之后,在面试Oppo
Android 事件分发介绍,之后,在面试Oppo
目录- 一、目的
- 二、环境
- 三、相关概念
- 3.1 事件分发
- 四、详细设计
- 4.1应用布局
- 4.1.1 应用布局结构
- 4.1.2 LayoutInspector
- 4.2 关键View&方法
- 4.2.1 相关View
- 4.2.2 相关方法
- 4.2.3 View与方法关系
- 4.3 事件分发概念图
- 4.3.1 事件分发类图
- 4.3.2 事件分发模型图
- 4.4 Activity组件
- 4.4.1 Activity->dispatchTouchEvent()
- 4.4.2 Activity->getWindow()
- 4.4.3 Activity->onTouchEvent()
- 4.5 ViewGroup组件
- 4.5.1 ViewGroup->dispatchTouchEvent()
- 4.5.2 ViewGroup->dispatchTransformedTouchEvent()
- 4.6 View组件
- 4.6.1 View->dispatchTouchEvent()
- 4.6.2 OnTouchListener->onTouch()
- 4.6.3 View->onTouchEvent()
- 4.7 例子-点击事件时序图
- 4.1应用布局
- 五、小结&问题点
- 六、代码仓库地址
- 七、参考资料
一、目的
最开始接触Android时,仅仅是知道Android系统存在的点击事件、触摸事件,但是并不清楚这些事件的由来。
之后,在面试Oppo和美图时,皆有问到Android的事件分发机制,但是都被问得很懵逼,归根到底都是对于其实现逻辑的不理解。
随后,想去弥补该模块的不足,浏览很多关于Android事件分发的博文,但仍存在一些疑惑,就想着去阅读下源码,整理下笔记,希望对同学们有帮助。
二、环境
- 版本:Android 11
- 平台:展锐 SPRD8541E
三、相关概念
3.1 事件分发
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
组件 | 描述 |
---|---|
Activity | Android事件分发的起始端,其为一个window窗口,内部持有Decorder视图,该视图为当前窗体的根节点,同时,它也是一个ViewGroup容器。 |
ViewGroup | Android中ViewGroup是一个布局容器,可以嵌套多个 ViewGroup 和 View,事件传递和拦截都由 ViewGroup 完成。 |
View | 事件传递的最末端,要么消费事件,要么不消费把事件传递给父容器 |
4.2.2 相关方法
方法 | 描述 |
---|---|
dispatchTouchEvent | 分发事件 |
onInterceptTouchEvent | 拦截事件 |
onTouchEvent | 触摸事件 |
4.2.3 View与方法关系
组件 | dispatchTouchEvent | onInterceptTouchEvent | onTouchEvent |
---|---|---|---|
Activity | ✔ | ❌ | ✔ |
ViewGroup | ✔ | ✔ | ✔ |
View | ✔ | ❌ | ✔ |
4.3 事件分发概念图
4.3.1 事件分发类图
4.3.2 事件分发模型图
Android的ACTION_DOWN事件分发如图,从1-9步骤,描述一个down事件的分发过程,如果大家能懂,就不用看下面文字描述了(写完这个篇幅,感觉文字好多,不好理解!)
- ACTION_DOWN事件触发。 当我们手指触摸屏幕,tp驱动会响应中断,通过ims输入系统,将down事件的相关信息发送到当前的窗口,即当前的Activity。
- Activity事件分发。 会引用dispatchTouchEvent()方法,对down事件分发。Activity本身会持有一个window对象,window对象的实现类PhoneWindow会持有一个DecorView对象,DecorView是一个ViewGroup对象,即我们可以理解为,Activity最终会将事件分发给下一个节点——ViewGroup。
- ViewGroup事件拦截。 ViewGroup接收到事件后,会先引用onInterceptTouchEvent(),查看当前的视图容器是否做事件拦截。
- ViewGroup消费事件。 如当前的ViewGroup对事件进行拦截,即会调用onTouchEvent(),对事件消费。
- ViewGroup事件不拦截。 则ViewGroup会继续遍历自身的子节点,并且当事件的坐标位于子节点上,则继续下发到下一个节点。ViewGroup的子节点有可能是View,也可能是ViewGroup(当然,ViewGroup最后也是继承于View的,突然感觉有点废话)。
- ViewGroup事件分发。 目标视图如果是ViewGroup,会引用其super类的dispatchTouchEvent()方法,即事件下发,不管目标视图是View或者ViewGroup最终引用的是View类的分发方法。
- View事件消费。 在View的dispatchTouchEvent()方法中会根据当前View是否可以点击、onTouch()是否消费、onTouchEvent()是否消费等条件,来判断当前是否为目标View。
- View事件未消费。 View事件未消费,则其父节点,即ViewGroup会调用onTouchEvent()方法,并根据返回值来决定是否消费事件。
- ViewGroup事件未消费。 ViewGroup事件未消费,择其父节点,即Actviity会调用onTouchEvent()方法
PS:
(1) ACTION_MOVE和ACTION_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的点击事件时序图,如果能够理解单击事件的由来,对于整个事件分发的知识要点已大体掌握。
五、小结&问题点
- 事件分发流程?包括ACTION_DWON、ACTION_UP、ACTION_MOVE事件的处理过程;
- ACTION_CANCEL事件的使用场景?父控件对move事件拦截场景?
- 单击、长按、触摸事件的产生过程?
- 点击一个View未抬起,同时move该事件直至离开当前View的范围,处理过程如何?
- 如果所有View都未消费事件,流程如何?
- ViewPage+ListView,左右滑动和上下滑动冲突的解决问题?即事件拦截过程?
- 普通的View是根据什么来决定是否消费事件,例如Button?
=>答:如无重写onTouchEvent事件,根据当前的View是否可点击,来决定是否消费事件。
=>答:如无重写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
用户评论