在现代生物学中,生命体除了需要具有自身繁殖、生长发育、新陈代谢、遗传变异等特性之外,还要具备一个必不可少的特性就是对外界刺激产生反应。同理,App就好似一个生命体,它也需要能够对外部事件进行响应处理,这也是本系列文章的主要讲解内容。
苹果的官方文档《Event Handling Guide for iOS》对事件处理做了非常详尽清晰的解释,建议大家仔细研读。本系列文章主要是对该文档的进一步解读和讨论,并加入了自己的理解。如果大家对事件处理有任何建议、意见或者疑问,请到我的CSDN博客留言:
http://blog.csdn.net/pucker
本篇文章主要介绍以下内容:
- 事件的种类
- 事件的处理流程
- 点击测试
- 高级事件处理
用户可以通过很多种方式和App进行交互,例如通过点击屏幕触发一个动作,通过旋转设备控制赛车方向,通过摇一摇设备来试试手气抓红包等。尽管交互方式多种多样,但iOS中的事件主要分为3大类:用户多点触屏事件、设备动作事件、外部设备控制事件。
用户通过点击屏幕来触发动作,这个常识性的操作很容易令人误解为是App能够直接感知到在某个视图上发生了某个事件。其实这只是我们看到的最终结果而已,具体的过程我们还是有必要深究一下:
其中前三个步骤是固定的,不需要我们参与。而步骤4才涉及到开发者的具体工作,即如何处理不同的事件,这也是本系列文章的重点。其中我主要想讲讲触屏事件的处理过程,对于设备动作事件与外部设备控制事件,如需了解请参阅苹果的官方文档《Event Handling Guide for iOS》。
刚才讲到了,触屏事件会交给用户所点击的视图来处理。你可能会问,有时App的界面非常复杂,如何确定用户所点击的究竟是哪个视图?没错,UIKit提供了一套规则,用来确定用户所点击的视图,即“点击测试”(Hit-Testing)。
在介绍点击测试之前,首先我们回顾一下iOS用户界面特征。构成App用户界面的视图具有父子关系,即:
显然,这是多叉树结构,因此不难得知点击测试算法具有递归性质。
除此之外,视图树还具有层次关系,即:
从常理上也不难得知,点击测试一定是要找到包含用户点击位置的,且位于视图树最上层的子视图,该视图就称作“点击测试视图”。
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:方法内部的实现,因此此图是我个人的理解,仅供参考。
点击测试显然基于以下事实:
点击测试视图如何处理触屏事件呢?大体分为两种方式:
我们应优选高级事件处理方式,内置控件或者手势识别器使得事件的处理变得简单不易出错,并且它们提供给用户统一直观的交互方式。而低级事件处理方式需要直接处理触屏,就需要添加额外的变量对触屏的过程和状态做标记,很麻烦且容易出错。不过,在某些情况下,直接处理触屏事件倒是最简单直接的手段。
所谓触屏,指的是一个手指从接触屏幕,在屏幕上移动,到离开屏幕的整个过程。
UIKit内置了多种控件来处理触屏,最常见的就是我们在Interface Builder界面设计器中Control拖拽一个按钮到代码中,创建一个IBAction方法来处理按钮的Touch Up Inside点击操作。我们也可以在代码中调用addTarget:action:forControlEvents:以及removeTarget:action:forControlEvents:方法来维护控件的目标行为表。
除了UIKit控件之外,手势识别器也是处理触屏事件的好帮手。手势识别器是UIGestureRecognizer类的实例,用于检测多点触屏序列(Multitouch Sequence)是否匹配对应的手势,例如单击(Tap)、滑动(Swipe)、旋转(Rotation)、缩放(Pinch)等标准手势。这里所说的多点触屏序列,指的是从第一个手指接触屏幕开始,到最后一个手指离开屏幕结束,之间所有的触屏动作的状态集合。
UIKit内置了6种手势识别器:
和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属性保存了手势识别器当前所处状态。手势识别器会严格按照下图所示来进行状态转换:
手势识别器的起始状态均为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等内容,敬请关注。