android 中输入事件分发详解

什么是输入事件?

我们知道,运行android系统的设备本质上是一台计算机,使用者在和计算机进行交互的时候可以抽象成简单的对计算机的输入和输出(IO)。那么对于运行在计算机上的操作系统来说,操作系统在与使用者进行交互的时候起始也是可以抽象成对外界的输入进行处理,然后在输出返还给使用者。本文只讨论的是android系统中的“输入事件”(因为本文讨论的都是输入事件,所以以下简称“输入事件”为“事件”)相关的内容。

根据以上描述,我们就可以回答上面的问题了:

1
事件是计算机设备接收到的输入信息。

有了以上的定义,我们先来研究下事件是怎么产生的?

在android系统中,事件是由用户的动作触发的,进而在android硬件层引起硬件状态的变化,android底层会将硬件的状态的变化封装成更加高级的类对象并传递到android 的Framework层,Framework会继续把事件传递给应用层(确切地说是应用层里面应用开发者编写的具体的View的子类对象),最终应用层层会根据事件来进行响应并将响应结果通过对硬件的调用反馈给用户。在以上的整个过程中,应用开发者只需要关注其中的一小部分就可以了,那就是事件从Framework层传递到应用层之后我们该怎么处理这些事件。当然,本文也主要讨论这些内容。

借用以下android 架构图来展示下事件从底层到应用层经历了多少过程。android架构图

注意:事件的传递并不是只传递一个单一的事件的,每一次用户的动作都会产生一个有序的事件列表,这个有序的事件列会被按顺序一次传递到应用层。

输入事件类型

android中所有的事件被抽象成了一个Java类,InputEvent。它有两个直接子类,MotionEvent 和 KeyEvent,分别代表了动作事件(通常由触摸屏所产生,也是最常见的)的事件和按键所产生的事件。但是事实上InputEvent不止有这两种。因为android系统可运行的设备类型非常丰富,在不同的设备上对应不同的硬件,肯定也就回有不同的输入事件了,InputEvent按照事件的输入来源不同可以分为以下几类(非全部):

  • Sensor InputEvent(传感器输入事件)
  • Hook InputEvent(耳机Hook键输入事件)
  • On InputEvent(开机键输入事件)
  • Touch Screen InputEvent(触摸屏输入事件,也是本文主要介绍的)
  • Key InputEvent(按键输入事件,Home/Menu/Back,电视遥控器按键等)
  • Hover InputEvent(鼠标动作输入事件)

一般来说,android系统的运行设备是手机或者平板电脑,它们都会有触摸屏,最常见的输入事件还是Touch Screen InputEvent了。本文将会主要介绍Touch Screen InputEvent 和 Key InputEvent。android系统的输入设备列表参考这里:输入设备

Touch Screen InputEvent的处理和传递

Touch Screen InputEvent基础

Touch Screen InputEvent被封装成了一个java类-MotionEvent,它包含三个基础的事件,其它的事件都是在者三个事件的基础之上进行组合封装而成的,例如,click事件就是一个ACTION_DOWN 和一个 ACTION_UP的组合。
事件名称 | 事件描述
—|—
ACTION_DOWN | 手指按到屏幕上
ACTION_MOVE | 手指在屏幕上移动
ACTION_UP | 手指从屏幕上离开

事件序列:

上文以及提到过,Touch Screen的事件并不是单个出现的,用户的每次交互都会产生一个事件序列,这个序列会以一个ACTION_DOWN开头,中间有0 或者 n个ACTION_MOVE事件,最后以一个ACTION_UP结尾。一个事件序列往往可以组成更加高级的事件,比如说点击事件、双击事件、手势事件等,View在消费这些事件的时候也会以一个独立的事件序列作为一个最小单位来消费,而不是一个个的基础事件。

所以,我们可以认识到,Touch Screen的事件是可以分为两种类型的:

  • 基础类型:包含ACTION_DOWN、ACTION_MOVE、ACTION_UP三个。
  • 复合类型:由基础类型的事件序列组合称一个代表了用户特定的输入信息的事件。

几个关键方法及其其作用

在软件开发中,当我们写下一个方法后,我们的目的可能是使用该方法完成某些功能,也有可能是为了程序的可扩展性等其它的理由。打个比方来说,在android的常用组件Activity类中,有一个方法叫做setContentView(),它的作用是给当前的Activity指定一个布局来显示,这样的方法就是我们刚提到的“来完成某些功能”的方法;Activity中还有一个叫做onCreate() 的方法,它的作用是一个占位的方法,表示现在是Activity正在创建的时机,可以在子类来重写该方法完成一些初始化工作,像这类方法并没有完成某些具体的功能,就是我们提到的“为了程序的可扩展性等其它的理由”。为了方便起见,在本文中我将会称第一种方法为“功能型方法”,称第二种为“结构型方法”。但是有些方法并不是简单地确认为某一种类型,它可能两种特性都包含。还以以上提到的Activity的onCreate方法为例,首先,它是个结构型方法,因为它的目的包括通知子类进行初始化工作,其次,它本身也进行了一部分对Activity的初始化,这样来说,他就也完成了某些功能,也可以说是一个功能型方法,事实上onCreate两者都是(以下简称该类方法为Both)。

在MotionEvent的处理和传递过程中有几个关键的方法,他们分别出现咋View、ViewGroup、Activity、Window等多个类中,但是名字则是完全一致,功能也是非常接近的,所以就在以下表格中统一介绍了。

方法名称 作用描述 方法类型 调用者
dispatchTouchEvent() 把事件分发给子类、onTouchListsner监听器、以及自身的onTouchEvent方法 功能型方法 View的该方法是由Window调用的,Window的该方法是由Activity调用的,Activity的该方法则是由WindowManagerService调用的
onTouchEvent() 类自身处理touchEvent,包括对OnClickListener的处理就是在这里处理的;让子类继承该方法来使得子类可以监听到touch Event的发生并处理事件 Both 同一个类的dispatchTouchEvent
onInterceptTouchEvent() 只在ViewGroup中才有,可以让子类实现是否拦截该事件不再往下一步传递,等于说是给ViewGroup的子类提供了一个打断事件传递的Hook占位方法 结构型方法 同一个类的dispatchTouchEvent

其实不难看出了,dispatchTouchEvent方法和onTouchEvent方法是成对出现的,这是在不同层级中传递事件的典型模型,先由dispatchTouchEvent方法把事件派发给下一个层级,然后下一个层级如果还以更下一级的话它会继续派发事件,直到最后没有再下一级了。这时就会在onTouchEvent中处理事件并向上一级返回自己的处理结果,然后上级再继续想上级汇报处理结果直到最顶级的那个类。这样就完成了一次完整的事件的派发和处理。

我们注意到,这几个方法都是有一个boolean型的返回值的,对于dispatchTouchEvent来说,它的返回值是指该类本身是否会消费掉该事件(只是表示,并不一定会是它指定的,因为再程序运行的时候还以监听器、子类重写等其它一系列因素会影响该类对事件的处理),为true表示该类会消费掉该事件。onTouchEvent的返回值表示该类是否真的消费了该事件,返回true表示真的消费了。此外的,onInterceptTouchEvent方法的返回值表示该ViewGroup会打断完整的事件的传播的链条,不再往它的下一级传递。

MotionEvent 在不同类之间的传递

以上提到了几个关键方法是MotionEvent事件分发和处理的关键所在,它们涉及到几个类:

  • Activity
  • Window
  • View
  • ViewGroup

那么,MotionEvent事件是如何在这几个类之间传递的呢?而且我们以及知道了事件是由Framework层传递到应用层的,那么究竟是谁从Framework层传递过来,又是谁在应用层接收到了呢?

通过查看android SDK的源码,可以发现是WindowManagerService这个关键类把事件从Framework层传递到应用层的,并且通过PhoneWindow的成员DecorView传递到了当前的Activity中(具体详情请参考Android FrameWork——Touch事件派发过程详解,本文不再赘述)。也就是说是WindowManagerService做了Framework层和应用层的桥梁,把MotionEvent事件传递给了当前的Activity,当前Activity是应用层最先接收到MotionEvent事件的类。

这时候,就可以理解为什么在Activity中有dispatchTouchEvent 和 onTouchEvent这两个类了,因为Activity是应用层事件分发的第一站。整个事件在应用层的传递如图所示:

应用层事件传递图示

MotionEvent在View中的传递

我们已经了解到了MotionEvent是怎么通过Activity传递到View中的,而android的View层是一个拥有树状结构的View树,树的每一个节点也都是一个View,那么MotionEvent是怎么在View树内部传递的呢?

首先,与该事件相关的根View的dispatchTouchEvent会被调用,一次来在该View内分发事件,事件会被分发给该类的onTouchEvent方法、该View的OnTouchListener的onTouch方法、以及子View的dispatchTouchEvent方法。View会依据以上所提到的三个目标方法的返回值来觉得dispatchTouchEvent的最终返回值。

然后,当事件被分发到onTouchEvent方法中以后,该View会对事件进行处理,View的OnClickListener的onClick方法便是在这里调用的。

假如说该View是一个ViewGroup,那么它的onInterceptTouchEvent方法会在dispatchTouchEvent方法执行的时候被调用,并根据是否打断事件传递来觉得dispatchTouchEvent的返回值。

以下伪代码可以清晰地展示它们三者的关系:

1
2
3
4
5
6
7
8
9
10
public boolean dispatchTouchEvent(MontionEvent e){
boolean consume = false;
if(onInterceptTouchEvent(e)){
consume = onTouchEvent(e);
}else{
consume = child.dispatchTouchEvent(e);
}
return consume;
}

当然,事实上View中的dispatchTouchEvent要复杂的多,它需要处理的东西非常多,这里我们就只挑主要的介绍了。

MotionEvent在View中传递的一些结论

  • MotionEvent事件序列是被当做一个整体来处理的,假如一个View不消耗一个ACTION_DOWN事件,那么随后的其它该序列的事件也将不会传递给它。
  • 当一个View觉得要处理某个时间序列的话,该事件序列的所有子事件都将会被交给它处理。
  • ViewGroup的onInterceptTouchEvent默认返回false,也就是说ViewGroup默认是不拦截事件的。

Key InputEvent的处理和传递

Key InputEvent 基础

在应用层,Key InputEvent被封装成了一个叫做KeyEvent的类,它想MotionEvent一样,也是InputEvent的一个子类。总的来说,KeyEvent在应用层的传递方式和Motion Event是一致的。都是由WindowManagerService获取到事件,由DecorView调用ViewRoot,进而按照Activity-Window-View树的流程传递事件的。

类似于MotionEvent的关键方法,KeyEvent的关键方法分别是:

  • dispatchKeyEvent
  • onKeyEvent
  • onInterceptKeyEvent

在View树中的传递过程与Touch事件的一致,这里不再赘述。

事件传递总结

通过以上分析不难发现:

  • android系统中的事件分发模型是一个典型的按层级分发的模型,使用两个关键的方法,dispatch… 和 on…Event,分别来分发事件和接收下一层级的反馈。在ViewGroup中都会提供一个onIntercept…Event结构型方法来表示该View希望随时可以打断事件往下面的继续分发。这三个关键的方法处理保证事件能够完整地被传递到任何View中,同时也给事件传递架构带来了很大的灵活性,开发者可以利用这几个方法很方便地达到自己特殊的事件处理目的。
  • android中的事件都可以分解成几个基础的事件,并把一系列基础的事件作为一个组合的高级事件来处理。也就是说不管多个复杂的交互输入事件,在系统中都可以被分解伪一系列的基础事件。
  • 每个事件序列都会又一个很明确的起始事件和一个明确的结束事件,比如Touch事件的ACTION_DOWN 和 ACTION_UP分别标识着一个Touch事件序列的起始和终止。这样就很好地保证了一个事件序列的完整性和独立性。

最后,以上所提到的只是android中事件分发机制的大致轮廓,具体的代码则要复杂的多,大家有时间的话可以研究下具体的源码,相信大家都会有很大的收获的。

参考资料:

https://developer.android.com/reference/android/view/MotionEvent.html

《Android开发艺术探索》第3章

http://blog.csdn.net/learnrose/article/details/6236890

http://blog.csdn.net/yanzi1225627/article/details/22592831

http://wangkuiwu.github.io/2015/01/01/TouchEvent-Introduce/

坚持原创技术分享,您的支持将鼓励我继续创作!