最近在使用Robolectric进行单元测试的时候经常需要模拟click、touch等UI操作,期间遇到了各种问题,例如:onTouch和onTouchEvent有什么区别?onClick什么时候会响应?使用ShadowView中clickOn模拟点击操作为什么无法响应onTouch?为什么ListView添加滑动菜单功能后,ListView本身不能再滑动?被这些问题困扰了很久,因此决定彻底搞清楚Android Touch事件分发机制。这篇文章先整体介绍了Android UI事件处理机制-基于监听的处理方式和基于回调的处理方式,然后分别从View、ViewGoup角度分析Touch事件背后的实现逻辑,最后结合Robolectric介绍如何在单元测试中测试相关的回调、监听方法。
Android UI事件处理机制
事件是指一个对象的状态发生变化,事件处理是指事件发生时执行的代码。与界面编程相关的就是UI事件-用户在程序界面中执行各种操作,之后应用程序需要对这些操作提供响应动作,这个响应就是由UI事件的处理机制来完成。
Android针对UI事件,提供了两种处理机制:一种是基于监听方式的处理机制,另一种是基于回调方式的处理机制。对于基于监听的处理方式,主要是为Android 界面组件绑定特定的事件监听器;对于基于回调的处理方式,主要的做法是通过为Android 组件重写�特定的回调方法实现。下面我们详细介绍二者具体模型、流程、使用方法。
基于监听的事件处理机制
基于监听的事件处理方式实际是一种委托式(代理)模式,某个组件(事件源)将整个事件的处理委托给特定的对象(绑定在该组件上的事件监听器),由这个特定的对象来进行该事件的响应。
事件监听处理模型
事件监听处理模型涉及到三类对象:
- 事件源-事件产生的地方,一般就是界面组件。
- 事件-事件封装了界面组件上的一次用户操作,如果程序需要获取界面组件上所发生事件的相关信息,就可以通过Event对象来获取。
- 事件监听器-包含事件处理方法,负责监听事件源所发生的事情,并对各种事件做出响应。
事件处理流程
简单来说,基于监听器的事件处理流程是事件源组件发生事件,系统会执行该事件源组件上监听器的对应处理方法。具体流程如下图所示。
常见的事件监听器实现方式
在事件处理模型三个重要组成部分中,事件由系统负责生成、任意界面组件都可作为事件源,而事件监听器是整个事件处理核心。因此我们主要的工作就是实现事件监听器。所谓实现事件监听器其实就是实现特定接口的Java类实例,常用的方法包括:
-
内部类实现
内部类实现事件监听器的优点是可以在当前外部类中复用该监听器类、监听器类是外部类的内部类,可以自由访问外部类的所有界面组件。
-
外部类实现
使用外部类定义事件监听器的形式比较少见,使用外部类实现的缺点主要是事件监听器属于特定GUI,定义成外部类不利于提高程序内聚性、外部类形式的事件监听器不能自由访问创建GUI界面的类中组件,编程不够简洁。
-
Activity实现
直接在Activity中实现监听器接口,这种方式虽然简单,但是却使得Activity类的职能混乱,Activity主要职责是完成界面初始化工作,但此时还需包含事件处理器方法,违反面向对象单职能原则。
-
匿名内部类实现
使用匿名内部类实现,代码简洁,是目前使用最广泛的方法。
基于回调的事件处理机制
与监听的委托式事件处理不一样,基于回调事件处理模型,事件源和事件监听器是统一的。用户在组件上激发某个事件,组件自己特定的方法将会负责处理该事件。通常实现的方法是通过继承组件类,重写相关的事件处理方法。
基于回调的事件传播
基于回调的事件处理方法都有一个boolean返回值,使用该返回值标识该回调方法是否已经完全消费该事件。
- true-该处理方法已经完全处理事件,该事件不会传播出去
- false-该处理方法并未完全处理该事件,该事件会传播出去
二者对比
基于监听器的事件处理机制优点:
- 事件源、事件监听器由两个类实现,使用委托(代理)模式,分工明确,易于维护。
- Android事件处理机制优先触发该组件绑定的事件监听器,然后才会触发该组件提供的事件回调方法。即监听机制优先级更高。
基于回调的事件处理机制优点:
- 适合处理事件逻辑比较固定的View
Android Touch 事件分发、响应机制
Android UI事件包括Touch事件、Key事件等,我们这里主要研究Touch事件分发、消费的流程。Touch事件分发消费流程中包括两个主角:View和ViewGroup(Activity的Touch事件实际上是调用它内部的ViewGroup的Touch事件)。Touch事件类型包括ACTION_DOWN,ACTION_UP,ACTION_MOVE。这里我们通过解析View、ViewGroup事件分发响应的源码,来对Android Touch事件的分发进行研究。
View Touch事件响应详细流程
View中关于Touch事件主要涉及两个方法:dispatchTouchEvent、onTouchEvent。Android View 事件分发机制 源码解析 (上)中结合实例及源码对这两个方法进行了详细研究,具体细节可参考该文章。
dispatchTouchEvent
View的dispatchTouchEvent主要就是将事件按照onTouchListener监听器、onTouchEvent事件回调的顺序依次分发。任意一个返回true则表示该事件已经被当前这个View处理了。再回过头来看上面我们提到的Android提供的两种事件处理方法优缺点中『Android事件处理机制优先触发该组件绑定的事件监听器,然后才会触发该组件提供的事件回调方法。即监听机制优先级更高。』,这段话是不是更能理解了?
onTouchEvent
View的onTouchEvent是Android系统处理Touch事件的回调方法,这个方法针对不同事件类型-ACTION_DOWN、ACTION_MOVE、ACTION_UP分别进行处理,我们下面也按照这三种事件类型进行分析。
-
ACTION_DOWN:更新View状态、发送一个检测长按的延时任务(500ms)。检测长按的延时任务是指
用户从ACTION_DOWN触发开始算起,如果500ms内没有抬起则认为产生了长按事件,这个时候就会触发长按事件的监听器 onLongClickListener的onLongClick方法执行,如果该方法返回为true,则设置mHasPerformedLongPress值为true,这个值 会在ACTION_UP事件中协助判断是否需要触发Click事件的监听器。如果onLongClick方法执行返回false或者是没有产生长按事件,mHasPerformedLongPress值仍为false。
ACTION_MOVE:这个事件的处理相对会比较简单,主要是判断是否移出了当前View,如果移出则取消在ACTION_DOWN中设置的View状态、取消长按操作检测等。
ACTION_UP:执行ACTION_UP后续步骤的前提条件是当前View的状态需要是pressed或者prepressed,即该View已经响应了ACTION_DOWN事件。然后通过判断mHasPerformedLongPress来决定是否对Click事件进行响应,如果mHasPerformedLongPress为true表示该View已经响应了长按事件,那么就不会再对Click事件进行处理;反之则表示需要处理Click事件,处理方法就是调用Click事件监听器onClickListener的onClick方法。最后清除View的状态,刷新页面完成该事件的处理。
总结
- 我们在屏幕上简单的一个触摸操作就会产生Touch事件、LongClick事件、Click事件。
- 当我们在屏幕上触摸时,第一个产生的事件就是Touch事件,。Android系统为Touch事件提供了两种处理机制-基于监听器方式(onTouchListener的onTouch处理方法)和基于回调的方式(onTouchEvent),其中优先处理监听器方式。
- 当ACTION_DOWN持续500ms以上,会产生LongClick事件。Android系统为LongClick事件提供了监听器方式(onLongClickLIstener的onLongClick处理方法)。
- 当ACTION_UP随ACTION_DOWN而来,就会产生Click事件。Android系统为Click事件也是提供了监听器方式(onClickListener的onClick处理方法)。但是Click事件的监听器是否触发则取决于该View是否消费了LongClick事件,如果未消费,那么则触发当前View 的Click事件监听器;如果已经消费了LongClick,则不会触发Click事件监听器。
下一篇将总结ViewGroup Touch事件处理流程及如何结合Robolectric在单元测试中测试Touch相关的回调方法、监听器。