Android 事件分发之追本溯源
前言
- Android设备的界面交互带来了非常好的体验,在我们日常使用中,无时无刻不在触发着事件的分发;比如点击了淘宝某个图片,比如点击了掘金APP的某个按钮,都会触发系统的事件分发;
- Android 事件分发也是自定义View很重要的一个知识点,搞懂事件分发,当自定义View或者解决滑动冲突的问题,都会显得胸有成竹了;
- 和以往结合源码的方式讲解相比,更想通过另一种有趣的角度来分析,接下来让我们正式开始吧;
1. 为什么要有事件分发机制?
1.1,Android手机
Android手机作为手持设备,界面显示区域并不是很大,为了有便携的效果,只能牺牲手机的显示区域;这就会带来一个问题,可视内容少;为了不影响用户体验,我们必须要在有限的区域做更多的展示,这就对界面的设计有很高的要求了;假如我们是Google的工程师,我们要怎么来设计界面,以此带来好的体验效果呢?
1.2,脑洞一
第一种设计:将界面显示区域切割,根据所需要显示的视图,切割为无数块,每一块对应着一部分视图;如下:
这种界面设计简单粗暴,需要多少个视图,就将界面切割成多少个视图模块,以此来放下所有的视图内容;当然,这样设计显而易见会有问题,当视图越来越多的时候,每一个视图的模块所能展示的区域就会越来越小,这样体验效果是肯定不行的;
1.3,脑洞二
第二种设计:既然通过切割显示区域以此来展示视图的方案有问题,那么我们就来试试重叠的效果吧;如下:
这种设计很好的解决了视图模块过多时,显示区域不够展示的问题;但是也会存在问题,每一个显示区域和用户的交互顺序混乱了,比如我要和模块为4的视图做交互,结果触发了视图5的交互效果,而脑洞一方案则没有该问题;既然如此,那么我们能不能针对脑洞二的方案来进行优化呢?
答案是:有的!
1.3,设计交互机制
当多个模块视图重叠时,要协调好与用户的交互就极其重要了,毕竟涉及到用户体验;
当用户的触碰屏幕的显示区域,我们并不知道哪个模块需要和用户进行交互,而我们又不能让用户和其中一个模块的交互失效,那么我们只能去遍历重叠的模块,由内部的视图来决定是否需要相应用户的操作;
这样就可以解决多个模块视图重叠时,哪个模块需要相应用户交互的问题了;
而这正是Android的事件分发机制;
当然上面只是我的脑洞,用于方便理解,如果你有更好的想法,可以和我交流;
那么这种机制是怎么来实现这种效果的呢?请继续往下看;
在深入分析事件分发之前,先来了解一下事件的来源;
2. 事件是什么,是怎么产生的?
2.1,事件的来源
当屏幕被触摸,Linux内核会将硬件产生的触摸事件包装为Event存到/dev/input/event[x]目录下。
接着,系统创建的一个InputReaderThread线程loop起来让EventHub调用getEvent()不断的从/dev/input/文件夹下读取输入事件。
然后InputReader则从EventHub中获得事件交给InputDispatcher。
而InputDispatcher又会把事件分发到需要的地方,比如ViewRootImpl的WindowInputEventReceiver中。
这里只是简单了解一下大概的流程,源码过于复杂,这里不做具体的分析;
概括之:当触摸屏幕的时候,硬件会捕捉到用户的触摸动作,告诉系统内核,系统内核将该事件保存下来,然后有一个线程会将这个事件读取出来,交由专门分发的类进行分发;
2.2,事件
当屏幕被触摸时,系统底层会将触摸事件(坐标和时间等)封装成MotionEvent事件返回给上层 View;从用户首次触摸屏幕开始,经历手指在屏幕表面的任何移动,直到手指离开屏幕时结束都会产生一系列事件;
MotionEvent的类型:
- MotionEvent.ACTION_DOWN:当屏幕检测到第一个触点按下之后就会触发到这个事件
- MotionEvent.ACTION_MOVE:当触点在屏幕上移动时触发;
- MotionEvent.ACTION_UP:当触点松开时被触发;
- MotionEvent.ACTION_CANCEL:由系统在需要的时候触发,不由用户直接触发;
3. 事件分发机制是怎么实现的?
3.1,设计模式
在分析事件分发机制之前,我们先来看一下事件分发涉及的设计模式;
这个设计模式是事件分发机制的核心,Google工程师是通过这个设计模式来设计事件分发机制的;理解了这个设计模式有助于我们理解事件分发机制;
而这个设计模式就是责任链模式;
3.2,责任链模式
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。
在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
下面我们通过一段伪代码来解读这个模式:
// 请求
switch (request) {
case 0:
// 对象一接收请求并处理
break;
case 1:
// 对象二接收请求并处理
break;
case 2:
// 对象三接收请求并处理
break;
case 3:
// 对象四接收请求并处理
break;
case 4:
// 对象五接收请求并处理
break;
default:
// 默认对象接收请求并处理
}
上面这个就是我们用的最熟悉的责任链模式,当有一个请求进入责任链的时候,会遍历当前责任链上所有的对象,如果匹配到了则提前结束遍历,如果匹配不到则会被默认的对象接收;
责任链的本质是一个单向的链表结构,当有请求进入时,只会单向传递,直到被接收;
3.3,具体实现
上面我们理解了责任链设计模式之后,接下来我们来看看事件分发机制的具体实现;
在上上篇博客里面分析了View的绘制流程,里面提到了View的层次关系,Activity是View的宿主,而最顶层的View是DecorView,而DecorView里面则是View树的结构,那么我们将这些关系一一对应到了责任链里面,来看看效果吧;
当有一个事件进入责任链时,会从最顶层的DecorView开始往View树传递,直到被其中一个对象所消费;
那么由此可知事件分发总共可以分为三个部分;
- Activity的事件分发;
- ViewGroup的事件分发;
- View的事件分发;
接下来先来看一下事件分发机制的核心方法,主要有三个;
- dispatchTouchEvent():传递事件,当前对象可以将事件通过这个方法传递给下一个对象;
- onInterceptTouchEvent():拦截事件;当前对象通过拦截事件,来终止事件的传递;
- onTouchEvent():处理事件,事件的最终去处;
下面我们通过Demo来看看事件是怎么传递的?
写了一个简单的布局,一个RelativeLayout里面放一个按钮;
接下来点击屏幕,看看流程会怎么走;
1,Activity的事件分发
step1:当点击屏幕的时候,会产出一个ACTION_DOWN的事件,传递到了Activity的dispatchTouchEvent方法里,来看一下Activity的dispatchTouchEvent方法,这里调用了super.dispatchTouchEvent(ev),也就是走了父类的dispatchTouchEvent方法;
step2:进入Activity的dispatchTouchEvent方法里面,看一下做了啥;
这里面有三个方法,第一个onUserInteraction()是空方法;
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* <p>All calls to your activitys {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
* <p>Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}
将注释翻译过来的意思就是:
每当Key,Touch,Trackball事件分发到当前Activity就会被调用。如果你想当你的Activity在运行的时候,能够得知用户正在与你的设备交互,你可以override该方法。
这个回调方法和onUserLeaveHint是为了帮助Activities智能的管理状态栏Notification;特别是为了帮助Activities在恰当的时间取消Notification。
所有Activity的onUserLeaveHint 回调都会伴随着onUserInteraction。这保证当用户相关的的操作都会被通知到,例如下拉下通知栏并点击其中的条目。
这个方法不是重点,不需要过多关注;
需要关注的是第二个方法getWindow().superDispatchTouchEvent(ev),这个方法最终走的是PhoneWindow的superDispatchTouchEvent();
step3:这个mDecor是DecorView,看看DecorView里的superDispatchTouchEvent(ev)方法做了啥?
这里面还是调的super,走的父类的方法;
最终走的是ViewGroup的dispatchTouchEvent()方法;在这个方法里面通过遍历当前所有的子View,通过子View的dispatchTouchEvent()方法将事件传递下去;ViewGroup的事件分发请看下面的分析;
到这里Acitivity事件就已经传递到ViewGroup了,如果后续的对象都没有处理该事件,即getWindow().superDispatchTouchEvent(ev)方法返回false时,Activity就会通过onTouchEvent()把当前的事件处理掉;
看一下Activity的onTouchEvent()里面做了啥?
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
// Window里面的方法;
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
Activity的onTouchEvent()会判断当前的事件是否在屏幕的边缘触发的,如果是,则返回true,否则返回false;
总结为流程图:
2,ViewGroup的事件分发
接下来我们来分析一下ViewGroup的事件分发;
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// step1;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
}
...
for (int i = childrenCount - 1; i >= 0; i--) {
...
// step2;
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
...
}
...
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
// 将当前的事件分发下去;
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
...
}
step1:在ViewGroup的dispatchTouchEvent()方法里面,在进行事件分发之前,会先调用onInterceptTouchEvent(ev)方法,用于判断当前的事件是否拦截,如果被拦截了,则事件不分发给子类了,如果没有拦截则继续分发下去;
这里需要注意的是,当事件为MotionEvent.ACTION_DOWN,才会走进onInterceptTouchEvent(ev)方法;
在走这个onInterceptTouchEvent(ev)方法之前,还有一个判断条件,disallowIntercept,这个条件是用来判断是否要禁用拦截事件,如果禁用了,则不会调用拦截的方法了;子类可以通过调用requestDisallowInterceptTouchEvent()方法修改;
如果ViewGroup的子类如果没有重写onInterceptTouchEvent(ev)这个方法,那么就会走ViewGroup的方法,这里用了4个判断条件,但是默认都是走的false,不拦截事件;
step2:如果事件没有被拦截,那么就会遍历当前所有的子View,然后调用子View的dispatchTouchEvent()方法,将事件分发下去;
那如果被拦截了,则会走super.dispatchTouchEvent(event)方法,也就是View的dispatchTouchEvent(event)方法;这个逻辑写在dispatchTransformedTouchEvent()方法里;
到这里ViewGroup的分发就讲完了,至于ViewGroup拦截事件后,怎么处理事件,请看下面的View事件分析;
流程图:
3,View的事件分发
View的事件分发也是调用的dispatchTouchEvent(event)方法,让我们来看一下这个方法的逻辑;
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
// step1
result = true;
}
if (!result && onTouchEvent(event)) {
// step2
result = true;
}
}
...
return result;
}
通过源码发现,当事件分发到了View的dispatchTouchEvent(event)后,事件就不会再继续分发下去了;那么这里面的逻辑是怎样的呢?
step1:先判断当前View的状态是可响应的((mViewFlags & ENABLED_MASK) == ENABLED),再判断触摸监听mOnTouchListener的onTouch()的返回值,如果子类实现了OnTouchListener这个监听,并且返回了true,那么dispatchTouchEvent(event)就会返回true,表示当前View已经处理该事件;
step2:判断当step1的状态为false时,则调用了onTouchEvent(event)来判断子类是否返回true,返回true则表示当前View已经处理该事件;
看一下onTouchEvent(event)的源码:
public boolean onTouchEvent(MotionEvent event) {
// 判断当前状态是否是可点击的
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:
...
performClickInternal();
break;
case MotionEvent.ACTION_DOWN:
...
checkForLongClick(0, x, y);
break;
...
}
return true;
}
return false;
}
这里需要关注的是MotionEvent.ACTION_DOWN和MotionEvent.ACTION_UP事件;
- MotionEvent.ACTION_UP:调用了performClickInternal()触发了点击监听的回调onClick(),这个是我们最常用的点击事件回调;具体是在performClick()方法里面实现的;
- MotionEvent.ACTION_DOWN:在这个判断里面,调用了checkForLongClick(0, x, y)触发了长按监听的回调,也就是onLongClick()方法;
通过判断当前的视图是否处于按压状态,且判断此视图添加的窗口数量是否和原始的一致,如果这两种状态都满足,就会触发长按监听回调;最终调用是在performLongClickInternal()方法里面;
流程图:
4. 总结
到这里,事件分发的流程就已经讲完了;
让我们来回忆一下上面提到的三个方法:
- dispatchTouchEvent(event):将事件传递给下一层,当传递到View这一层的时候,就不会再继续往下传了;
- onInterceptTouchEvent(ev):将事件拦截下来,只有ViewGroup有这个方法,当拦截后,就会走View的dispatchTouchEvent(event)方法来处理事件;
- onTouchEvent(event):处理事件,在Activity层时,只有触摸边界的时候才会处理事件,在ViewGroup和View层时,会先判断是否有touch监听,没有的话,才会触发这个方法去处理事件;
分析到这里,关于上面脑洞一的设计,这种分发机制是不是完美的解决了交互的问题;
无论你视图重叠多少,事件都会一层层的传递过去,直到被某一层处理掉;有了这个机制,Android的界面就变的更灵活,更有创造性了;
看一下汇总的流程图:
关于自定义View相关的文章,之前也总结了几篇,感兴趣的可以看一下;
参考&感谢
- Android事件分发完全解析之为什么是她
- 原来Android触控机制竟是这样的?
- Java设计模式 - 责任链模式
- 责任链模式
- Android事件分发机制详解:史上最全面、最易懂
- Android 触摸事件分发机制(一)从内核到应用 一切的开始