探秘 iOS 事件响应机制:解锁更优秀的应用交互设计!

在 App 开发过程中,事件是用户和 App 沟通互动的桥梁。如果开发者没有妥善处理好事件,不仅会增加用户使用 App 的成本,同时还会引来用户的不满和抱怨。因此,妥善处理好用户发出的事件是每个 iOS 开发者的必修课。今天,我们就来聊聊 iOS 的事件响应机制。

事件

UIEvent 是 iOS 中用于描述事件的对象,它包含了与用户交互相关的信息,例如触摸事件、加速度事件等。UIEvent 对象描述了一系列 UITouch 对象的集合,每个 UITouch 对象表示一个触摸点的状态和属性。如下代码所示:

@interface UIEvent : NSObject 
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches; // 触摸事件所包含的所有 Touch 
@property(nonatomic,readonly) UIEventType type; // 事件类型,主要包含 touch、motion、远程或 3d-touch 等 
@end

App 可以接受多种类型的 UIEvent,包括触摸事件、运动事件、远程控制事件和物理按压事件等。这些事件都可以通过 UIEventType 来描述事件类型,常见的事件类型包括:

  • UIEventTypeTouches 是触摸事件,通常触碰屏幕;
  • UIEventTypeMotion 是设备运动事件,比如摇晃手机;
  • UIEventTypeRemoteControl 是远程控制事件,比如用 AirDrop 控制播放和暂停;
  • UIEventTypeRemoteControl 是物理按压事件,比如 3D Touch,iOS 9 以上支持。

无论哪种事件,都需要有事件响应者来响应事件,接下来我们来聊聊下事件的响应者。

响应者

UIResponder 是用于描述响应者,是一个抽象基类,它是所有能够响应事件的对象的基类,是构成 UIKit 事件处理的核心,包括UIApplication、UIWindow、UIViewController、UIView等都是事件响应者之一。UIResponder 类中定义了一系列的事件处理方法,包括触摸事件、运动事件、远程控制事件等,子类可以重写这些方法来处理对应类型的事件。例如触摸事件的方法有:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; 

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; 

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; 

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

除了处理事件外,UIResponder 还负责管理响应链上事件的传递。当事件发生时,系统会将事件传递给第一响应者,如果当前响应者对象无法处理该事件,则将事件转发给响应链上下一个响应者对象,直到事件被处理或者事件传递到响应链的尾部。

在响应链中,每个响应者对象都可以通过 nextResponder 属性获取它的下一个响应者对象。当事件传递到当前响应者对象时,该响应者对象可以选择处理该事件并停止事件传递,或者将事件转发给下一个响应者对象进行处理。如果所有的响应者对象都无法处理该事件,则该事件最终会被系统处理。

手势识别器

在 iOS 开发中,手势识别器(UIGestureRecognizer)是一种特殊的“响应者”,它们能够识别用户在屏幕上的手势操作,例如轻点、拖动、捏合等。UIGestureRecognizer 通常与视图对象一起使用,可以将手势识别器添加到视图对象上,并在识别到手势时,将识别结果传递给该视图对象或其父视图作为事件响应者进行处理。通过使用手势识别器,可以方便地处理用户手势操作,并实现一些复杂的交互效果,从而提高应用程序的用户体验。

UIGestureRecognizer 可以分为两大类:间断手势(例如 Tap 手势)和连续手势(例如 Pinch 手势)。间断手势是指一些短暂的手势,例如轻击、双击、长按等,这些手势通常是瞬间发生的,用户通过触摸屏幕来触发。连续手势是指一些持续的手势,例如拖动、捏合、旋转等。

UIGestureRecognizer 的识别过程是一个状态机的过程,从一个状态跳转到另一个状态。每个手势识别器都有一个或多个状态,用于表示手势的不同阶段。手势识别器会根据用户的手势操作,依次进入不同的状态,并在每个状态中进行判断和处理。

对于间断手势,一般一开始是处于 Possible 状态,如果识别成功则进入 Recognized 态,识别不成功则进入 Failed 态,如下图所示:

iOS-UIEvent.jpg

对于连续手势,一开始同样也是 Possible 状态,一旦开始识别成功就进入 Began 态,失败则进入 Failed 态,接着会转到 Changed 态,若识别过程发现于预期不符合,则进入 Canceled 态,符合预期则进入 Recognized 态,如下图所示:

iOS-UIEvent (1).jpg

总的来说,UIGestureRecognizer 是一种非常强大的手势识别器,它可以帮助开发者快速实现各种手势操作,并提高应用程序的用户体验。

事件响应机制

事件的响应过程是串联起事件和响应者的核心流程,从前面我们也知道事件有很多种类,其中最常用的是触摸事件(Touch Events),我们以它作主角,来好好理解下事件响应过程。

在 iOS 中,当用户触摸屏幕时,负责管理硬件和驱动程序的 IOKit 会将用户的触摸封装成一个 IOHDEvent 对象,这是一个最初始的触摸事件对象。IOKit 然后会将 IOHDEvent 对象通过 mach port(进程间通信)转发给 SpringBoard(iPhone 的主屏幕程序)。接着 SpringBoard 会通过通过 mach port 将 IOHDEvent 对象转发给当前 App 的主线程。

当前 App 主线程关联的主 RunLoop 收到 SpringBoard 的消息后,会被唤醒,触发 Source1 回调 __IOHIDEventSystemClientQueueCallback() 处理事件。Source1 的回调内部会触发 Source0 的回调 __UIApplicationHandleEventQueue(),将 IOHDEvent 对象转化为 UIEvent 对象,并通过 UIApplication 对象发送给 UIWindow 对象。

UIWindow 对象收到 UIEvent 对象后,需要先通过 Hit-Test 找到合适的响应者对象来处理事件。

Hit-Test 过程

在 iOS 应用程序中,所有的视图对象都是按照一定的层次结构组织起来的,形成了一个树状的集合。UIApplication 在接收到触摸事件后,会先将该事件发送给最上层可见的 UIWindow 对象,并通过 Hit-Test 算法来确定需要响应该事件的视图对象。

Hit-Test 可以理解成一个探测器,它从 UIWindow 的根视图开始遍历整个 UIView 层次结构,递归地访问每个 UIView 对象,并判断每个 UIView 对象是否可以成为响应者。如果可以,则将该 UIView 对象作为候选响应者对象加入响应链。如果当前 UIView 对象有 subviews,则会对其 subview 进行 Hit-Test,直到找到最合适的响应者对象为止。最终,如果没有找到合适的响应者对象,则事件会被丢弃。Hit-Test 的核心主要涉及两个函数,如下代码所示:

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

UIWindow 对象会先开始对自己做 Hit-Test,若触摸点在 UIWindow 对象上,会让 subviews 里面的所有子 View 也进行 Hit-Test,直至找到最终的响应者 UIView 对象。当 Hit-Test 结束后并成功找到第一响应者 UIView 对象后,UIView 对象和对象上的手势识别器都会和 UITouch 对象关联起来,后续就可以直接将触摸事件发送给 UIView 对象和 对象上的手势识别器,Hit-Test 的过程如下图所示:

iOS-Hit-Test过程.jpg

前面我们提到 Hit-Test 的核心主要涉及两个方法,分别是 hitTest:withEvent: 和 pointInside:withEvent:,这两个方法承担着不同的职责。其中,pointInside:withEvent: 主要用于检查触摸事件的触摸点是否在当前 View 对象的范围内,而 hitTest:withEvent: 通过调用 pointInside:withEvent: 判断触摸点是否在 View 对象的范围内,同时还需要判断当前 View 对象 isUserInterface、isHidden、alpha 状态和让 subviews 也递归进行 Hit-Test 的职责,如下代码所示:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    //判断自己能否接收事件 
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) { 
        //不能接收事件 return nil; 
    } 
    //点在不在自己身上 
    if (![self pointInside:point withEvent:event]) { return nil; } 
    //从后往前遍历自己的子控件,把事件传递给子控件,调用子控件的hitTest, 
    int count = (int)self.subviews.count; 
    for (int i = count - 1; i >= 0; i--) { 
        //获取子控件 
        UIView *childView = self.subviews[i]; 
        //把当前点的坐标系转换成子控件的坐标系 
        CGPoint childP = [self convertPoint:point toView:childView]; 
        UIView *fitView = [childView hitTest:childP withEvent:event]; 
        if (fitView) { return fitView; } 
    } //如果子控件没有找到最适合的View,那么自己就是最适合的View. 
    return self; 
}

事件响应和传递

经过 Hit-Test 过程,系统可以找到合适的第一响应者 UIView 对象和 UIView 对象关联的所有手势识别器,并将它们关联到 UITouch 对象上。之后,触摸事件就可以通过 [UIWindow sendEvent:] 直接发送给该 UIView 对象和所有手势识别器,如下图所示:

截图.png

UIView 对象提供了四个方法去响应触摸事件,如下代码所示:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; 

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; 

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; 

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

第一响应者 UIView 对象不仅可以选择对触摸事件进行响应,还可以决定是否将触摸事件沿着响应链向父 View 传递,通过重写 touchesBegan:withEvent: 系列方法,在方法内部调用 [super touchesBegan:withEvent:],即可将触摸事件传递给父 View;若不在方法内部调用 [super touchesBegan:withEvent:],则父 View 无法收到触摸事件。

响应者优先级

我们知道,UIView 对象还可以关联一些列手势识别器,手势识别器也可以作为响应者,因此 UIView 对象和关联的手势识别器存在优先级关系。那它们的优先级是怎么样的呢?

UIView 对象和间断手势

假如我们创建了一个 RedView 对象,并关联了一个 UITapGestureRecognizer 对象,并发起了一个触摸事件,如下图所示:

iOS-第 2 页.jpg
当触摸事件发生时,系统会将事件发送给最上层的 UIWindow 对象,并通过 Hit-Test 确定 RedView 对象是第一响应者,系统会将该触摸事件同时发送给 UITapGestureRecoginzer 对象和 RedView 对象。

UITapGestureRecoginzer 对象会开始识别手势, RedView 对象也会调用 touchesBegan 方法响应事件。若 UITapGestureRecoginzer 对象成功识别了触摸事件,会触发 RedView 对象调用 touchesCancelled 方法取消对事件的响应,之后的触摸事件都由 UITapGestureRecoginzer 对象处理。如下所示:

"RedView invoke touchesBegan:withEvent:" 

"RedView invoke touchesMoved:withEvent:" 
"UITapGestureRecognizer invoke tap" 
"RedView invoke touchesCancelled:withEvent:"

若 UITapGestureRecoginzer 对象识别手势失败后,系统会将事件会交由 RedView 对象处理。由此可见,间断手势识别器对象的响应优先级高于 UIView 对象。

UIView 对象和连续手势

假如我们的 RedView 对象,关联了一个 UILongPressGestureRecognizer 对象,如下图所示:

iOS-第 2 页 (1).jpg

当触摸事件发生时,系统会将事件发送给最上层的 UIWindow 对象,并通过 Hit-Test 确定 RedView 对象是第一响应者,系统会将该触摸事件同时发送给 UILongPressGestureRecognizer 对象和 RedView 对象。

UILongPressGestureRecognizer 对象会开始识别手势,由于 UILongPressGestureRecognizer 对象是一个连续手势, RedView 对象也会多次触发 touchBegan 和 touchMoved 响应事件。

若 UILongPressGestureRecognizer 对象成功识别触摸事件,会从 possible 状态转为 began 状态,同时触发 RedView 对象调用 touchesCancelled 方法取消对后续事件的响应,紧接着 UILongPressGestureRecognizer 对象会继续识别一次进入 changed 状态,当事件结束后会进入 ended 状态,如下所示:

"RedView invoke touchesBegan:withEvent:" 

"RedView invoke touchesBegan:withEvent:" 
"RedView invoke touchesMoved:withEvent:" 
"RedView invoke touchesMoved:withEvent:" 
"UILongPressGestureRecognizer invoke state = began" 
"RedView invoke touchesCancelled:withEvent:" 
"UILongPressGestureRecognizer invoke state = changed" 
"UILongPressGestureRecognizer invoke state = ended"

UILongPressGestureRecognizer 对象识别失败后,会让 RedView 对象继续进行事件响应。由此可见,连续手势识别器对象的响应优先级高于 UIView 对象。

总结

本文主要介绍了 iOS 中的事件响应机制。首先讲解了事件对象和事件类型,其中包括触摸事件、运动事件、远程事件和物理按压事件等。然后介绍了事件响应者,即负责响应事件的对象,大多数情况下事件响应者都是 UIResponder 的子类,但也有一类特殊的“响应者”,即手势识别器(UIGestureRecognizer)。

接着,本文介绍了事件响应的过程,其中最重要的是 Hit-Test ,通过 Hit-Test 可以找到合适的第一响应者对象,并将事件发送给该对象进行响应和向上传递。以触摸事件为例,系统会将触摸事件发送给最上层的 UIWindow 对象,并通过 Hit-Test 找到需要响应该事件的第一响应者对象。

最后,本文对 UIView 对象和手势识别器的响应优先级进行了比较:

  • 手势识别器的优先级比 UIView 高,如果成功识别手势,则通知 UIApplication 取消第一响应者 UIView 对象对事件的响应,并停止向第一响应者发送事件。
  • 如果手势识别器未能识别手势,而此时触摸并未结束,则停止向手势识别器发送事件,仅向第一响应者发送后续事件。
  • 如果手势识别器未能识别手势,且此时触摸已经结束,则向第一响应者发送 end 状态的touch事件,以停止对事件的响应。
    虽然手势识别器的响应优先级高于 UIView 对象。但是,响应优先级还受到触摸事件处理方法的实现方式和调用顺序的影响。

希望通过本文的分享,读者能够更好地理解 iOS 的事件响应机制,为开发 iOS 应用程序提供帮助。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY0kb69d' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片