iOS事件处理系列1-事件的种类与处理流程

在现代生物学中,生命体除了需要具有自身繁殖、生长发育、新陈代谢、遗传变异等特性之外,还要具备一个必不可少的特性就是对外界刺激产生反应。同理,App就好似一个生命体,它也需要能够对外部事件进行响应处理,这也是本系列文章的主要讲解内容。

苹果的官方文档《Event Handling Guide for iOS》对事件处理做了非常详尽清晰的解释,建议大家仔细研读。本系列文章主要是对该文档的进一步解读和讨论,并加入了自己的理解。如果大家对事件处理有任何建议、意见或者疑问,请到我的CSDN博客留言:
http://blog.csdn.net/pucker

本篇文章主要介绍以下内容:
- 事件的种类
- 事件的处理流程
- 点击测试
- 高级事件处理

一、事件的种类

用户可以通过很多种方式和App进行交互,例如通过点击屏幕触发一个动作,通过旋转设备控制赛车方向,通过摇一摇设备来试试手气抓红包等。尽管交互方式多种多样,但iOS中的事件主要分为3大类:用户多点触屏事件、设备动作事件、外部设备控制事件。

  • 用户多点触屏事件(简称触屏事件),也就是iOS设备屏幕感知到用户的手指在屏幕上按下、移动、离开的动作而生成的事件信息。例如点击按钮、通过手势缩放图片、拖动上下滚动网页等。
  • 设备动作事件,也就是当我们移动、旋转iOS设备时,设备内部的加速仪、陀螺仪、磁强仪等硬件感知到的事件信息。例如摇一摇红包、通过旋转设备控制赛车方向、指南针等。
  • 外部设备控制事件,主要来自其他外部设备,例如耳机的线控、外接手柄、遥控器等。

二、事件的处理流程

用户通过点击屏幕来触发动作,这个常识性的操作很容易令人误解为是App能够直接感知到在某个视图上发生了某个事件。其实这只是我们看到的最终结果而已,具体的过程我们还是有必要深究一下:

  1. 收集事件数据。不论是什么类型的事件,首先都是硬件先感知并生成对应的数据,然后交给iOS操作系统。以触屏事件为例,首先是屏幕硬件感受到了手指对其施加的压力,然后生成相关信息数据(位置坐标值、压力数据等)并交给iOS。
  2. 分发事件数据。从操作系统的层面来说,它压根就不知道,也不关心你这个App长什么样,它只负责将相关信息数据封装为事件对象,并放入当前正在运行的App的消息队列中。
  3. 按序取出事件。App在启动之后,应用程序对象会一直监听消息队列。一旦队列中有待处理的事件对象,它就会从消息队列中取出事件对象,并交给关键窗口对象(Key
    Window)。
  4. 交给合适的对象来处理事件。关键窗口对象会根据事件的类型,交给不同的对象来处理事件。简单来说,触屏事件会先交给用户所点击的视图来处理;设备动作事件与外部设备控制事件会交给某个指定的对象来处理。

其中前三个步骤是固定的,不需要我们参与。而步骤4才涉及到开发者的具体工作,即如何处理不同的事件,这也是本系列文章的重点。其中我主要想讲讲触屏事件的处理过程,对于设备动作事件与外部设备控制事件,如需了解请参阅苹果的官方文档《Event Handling Guide for iOS》。

刚才讲到了,触屏事件会交给用户所点击的视图来处理。你可能会问,有时App的界面非常复杂,如何确定用户所点击的究竟是哪个视图?没错,UIKit提供了一套规则,用来确定用户所点击的视图,即“点击测试”(Hit-Testing)。

三、点击测试

在介绍点击测试之前,首先我们回顾一下iOS用户界面特征。构成App用户界面的视图具有父子关系,即:

  • 视图可以有0个或者多个子视图。
  • 除了根视图(窗口)没有父视图外,其余的视图只能有唯一一个父视图。

显然,这是多叉树结构,因此不难得知点击测试算法具有递归性质。

除此之外,视图树还具有层次关系,即:

  • 子视图位于其父视图之上。
  • 同级视图(兄弟视图)之间具有上下层次顺序。

从常理上也不难得知,点击测试一定是要找到包含用户点击位置的,且位于视图树最上层的子视图,该视图就称作“点击测试视图”。

UIView类定义了两个实例方法:

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

其中hitTest:withEvent:用于点击测试,如果找到则返回点击测试视图,否则返回nil。该方法内部会递归进行调用。pointInside:withEvent:用于判断某个点是否在视图中。具体算法细节我就不在此赘述了,我画了一个点击测试流程图。注:由于我们无法了解到hitTest:withEvent:方法内部的实现,因此此图是我个人的理解,仅供参考。

iOS事件处理系列1-事件的种类与处理流程_第1张图片

点击测试显然基于以下事实:

  • 如果视图禁止接收用户交互、隐藏或者透明度非常接近0,则点击视图为空。
  • 如果视图都没有包含触屏位置,则显然点击视图为空。
  • 如果视图包含触屏位置,且没有子视图或者所有子视图的点击视图为空,则点击视图为其本身。
  • 应逆序遍历subviews数组。

点击测试视图如何处理触屏事件呢?大体分为两种方式:

  • 高级事件处理:利用UIKit提供的各种用户控件或者手势识别器来处理事件。
  • 低级事件处理:在UIView的子类中重写触屏回调方法,直接处理触屏事件。

我们应优选高级事件处理方式,内置控件或者手势识别器使得事件的处理变得简单不易出错,并且它们提供给用户统一直观的交互方式。而低级事件处理方式需要直接处理触屏,就需要添加额外的变量对触屏的过程和状态做标记,很麻烦且容易出错。不过,在某些情况下,直接处理触屏事件倒是最简单直接的手段。

四、高级事件处理

所谓触屏,指的是一个手指从接触屏幕,在屏幕上移动,到离开屏幕的整个过程。

UIKit内置了多种控件来处理触屏,最常见的就是我们在Interface Builder界面设计器中Control拖拽一个按钮到代码中,创建一个IBAction方法来处理按钮的Touch Up Inside点击操作。我们也可以在代码中调用addTarget:action:forControlEvents:以及removeTarget:action:forControlEvents:方法来维护控件的目标行为表。

除了UIKit控件之外,手势识别器也是处理触屏事件的好帮手。手势识别器是UIGestureRecognizer类的实例,用于检测多点触屏序列(Multitouch Sequence)是否匹配对应的手势,例如单击(Tap)、滑动(Swipe)、旋转(Rotation)、缩放(Pinch)等标准手势。这里所说的多点触屏序列,指的是从第一个手指接触屏幕开始,到最后一个手指离开屏幕结束,之间所有的触屏动作的状态集合。

UIKit内置了6种手势识别器:

  • UITapGestureRecognizer:点击(单击、双击、三连击等)手势。
  • UIPinchGestureRecognizer:缩放手势。
  • UIPanGestureRecognizer:拖拽手势。
  • UISwipeGestureRecognizer:滑动手势。
  • UIRotationGestureRecognizer:旋转手势。
  • UILongPressGestureRecognizer:长按手势。

和UIKit控件类似,手势识别器内部也使用目标行为表:

@interface UIGestureRecognizer : NSObject
...
- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action NS_DESIGNATED_INITIALIZER;

- (void)addTarget:(id)target action:(SEL)action;    

- (void)removeTarget:(nullable id)target action:(nullable SEL)action;
...

手势分为离散手势(Discrete Gesture)和连续手势(Continuous Gesture),例如点击操作是一个瞬间动作,因此点击手势是一个离散手势;缩放手势包含多个手指的一系列动作,因此缩放手势是一个连续手势。离散手势在识别后只发送一次消息,而连续手势在识别过程中会随触屏的变化发送多条消息,直至多点触屏序列结束。

手势识别器是一个有限状态机(Finite State Machine),它会接收并分析视图上的多点触屏序列,针对不同情况按照确定的方式进行内部的状态转换:

@interface UIGestureRecognizer : NSObject
...
@property(nonatomic,readonly) UIGestureRecognizerState state;
...

typedef NS_ENUM(NSInteger, UIGestureRecognizerState)
{
    UIGestureRecognizerStatePossible,
    UIGestureRecognizerStateBegan,
    UIGestureRecognizerStateChanged,
    UIGestureRecognizerStateEnded,
    UIGestureRecognizerStateCancelled,
    UIGestureRecognizerStateFailed,
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};

UIGestureRecognizer类的state属性保存了手势识别器当前所处状态。手势识别器会严格按照下图所示来进行状态转换:

iOS事件处理系列1-事件的种类与处理流程_第2张图片

手势识别器的起始状态均为Possible。上图左侧为离散手势,右侧为连续手势。除了转换到
Failed或者Cancelled之外,手势识别器在每次状态转换时都会发送消息。当识别结束后(Recognized、Failed或者Cancelled),状态又会恢复到起始状态Possible。

手势识别器需要附加到视图上才能识别手势:

@interface UIView (UIViewGestureRecognizers)
@property(nullable, nonatomic,copy) NSArray<__kindof UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);

- (void)addGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer NS_AVAILABLE_IOS(3_2);
- (void)removeGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer NS_AVAILABLE_IOS(3_2);
...
@end

在IB中可以将手势识别器拖拽到某个视图上。在代码中可以调用initWithTarget:action:创建手势识别器实例,然后调用
UIView的addGestureRecognizer:和removeGestureRecognizer:实例方法添加或移除手势识别器。注意到上面的gestureRecognizers属性是一个手势识别器数组,说明视图允许同时附加多个手势识别器,例如我们希望同时处理单击和滑动手势。

下一篇文章会继续介绍手势识别器的使用方法。除此之外,后续文章还会讲解响应者、手势识别器以及iOS 9新添加的3D Touch等内容,敬请关注。

你可能感兴趣的:(ios,事件,event,苹果)