事件传递:响应者链
当用户生成的事件发生时,UIKit会创建一个事件对象,其中包含处理事件所需的信息。然后它将事件对象放入活动应用程序的事件队列中。对于触摸事件,该对象是一组打包在UIEvent对象中的触摸。对于运动事件,事件对象根据您使用的框架和您感兴趣的运动事件类型而异。
事件沿着特定路径传播,直到将其传递到可以处理它的对象。首先,单例UIApplication对象从队列的顶部获取一个事件并调度它进行处理。通常,它将事件发送到应用程序的关键窗口对象(key window),该对象将事件传递给初始对象进行处理。最初的对象取决于事件的类型。初始对象取决于事件的类型。
- 触摸事件。对于触摸事件,窗口对象首先尝试将事件传递到发生触摸的视图。这种视图被称为命中测试(hit-test)视图。
- 运动和远程控制事件。通过这些事件,窗口对象将摇动或遥控事件发送给第一响应者进行处理。
这些事件路径的最终目标是找到一个可以处理和响应事件的对象。因此,UIKit首先将事件发送到最适合处理事件的对象。对于触摸事件,该对象是命中测试视图,对于其他事件,该对象是第一响应者。
命中测试返回触摸发生位置的视图
iOS使用命中测试来查找触摸下的视图。命中测试涉及检查触摸是否在任何相关视图对象的范围内。如果是,则递归检查该视图的所有子视图。包含触摸点的视图层次结构中的最低视图将成为命中测试视图。在iOS确定命中测试视图后,它会将触摸事件传递到该视图进行处理。
为了说明,假设用户触及图2-1中的视图E.iOS通过按以下顺序检查子视图来查找命中测试视图:
- 触摸在视图A的范围内,因此它检查子视图B和C.
- 触摸不在视图B的范围内,但它在视图C的范围内,因此它检查子视图D和E.
- 触摸不在视图D的范围内,但它在视图E的范围内。 视图E是包含触摸的视图层次结构中的最低视图,因此它将成为命中测试视图。
图2-1 命中测试返回被触摸的子视图
hitTest:withEvent:
方法返回给定CGPoint和UIEvent的命中测试视图。hitTest:withEvent:方法首先调用本身的pointInside:withEvent:方法。如果传入hitTest:withEvent:的点在视图的边界(bounds)内,则pointInside:withEvent:返回YES。然后,该方法在每个返回YES的子视图上递归调用hitTest:withEvent:。
如果传入hitTest:withEvent:的点不在视图边界内,则第一次调用pointInside:withEvent:方法返回NO,该点将被忽略,hitTest:withEvent:返回nil。如果子视图返回NO,那么视图层次结构的整个分支将被忽略,因为如果在该子视图中没有发生触摸,它也不会发生在该子视图的任何子视图中。这意味着在其子视图之外的子视图中的任何点都不能接收触摸事件,因为触摸点必须位于超视图和子视图的界限内。如果子视图的clipsToBounds属性设置为NO,则会发生这种情况。
命中测试视图提供了处理触摸事件的第一个机会。如果命中测试视图无法处理事件,则该事件沿着该视图的响应者链延伸,直到系统找到可以处理它的对象。
响应者链由响应者对象组成
许多类型的事件都依赖于响应者链来进行事件传递。 响应者链是一系列链接的响应者对象。它从第一响应者开始并以应用程序对象结束。 如果第一响应者不能处理事件,它将该事件转发给响应者链中的下一响应者。
响应者对象是可以响应和处理事件的对象。UIResponder类是所有响应者对象的基类,它定义了编程接口不仅用于事件处理,还用于常见的响应者行为。UIApplication,UIViewController和UIView类的实例是响应者,这意味着所有视图和大多数关键控制器对象都是响应者。请注意,Core Animation图层不是响应者。
第一响应者被指定首先接收事件。通常,第一个响应者是一个视图对象。 一个对象通过做两件事成为第一响应者:
- 覆盖canBecomeFirstResponder方法返回YES。
- 收到becomeFirstResponder消息。 如有必要,对象可以将自己的消息发送给自己。
事件不是唯一依赖响应者链的对象。响应者链用于以下所有方面:
- 触摸事件。如果命中测试视图无法处理触摸事件,则事件会从命中测试视图开始传递到响应者链。
- 运动事件。要使用UIKit处理抖动事件,第一响应者必须实现UIResponder类的motionBegan:withEvent:或motionEnded:withEvent:方法。
- 远程控制事件。为了处理远程控制事件,第一响应者必须实现UIResponder类的remoteControlReceivedWithEvent:方法。
- 动作消息。当用户操纵某个控件(如按钮或开关),并且操作方法的目标为零时,将通过以控制视图开始的响应者链发送该消息。
- 编辑菜单消息。当用户点击编辑菜单的命令时,iOS使用响应者链来查找实现必要方法的对象(如
cut:
,copy:
和paste:
)。 - 文本编辑。当用户点击文本字段或文本视图时,该视图自动成为第一响应者。默认情况下,出现虚拟键盘,文本字段或文本视图成为编辑的焦点。如果适用于您的应用,您可以显示自定义输入视图而不是键盘。您还可以将自定义输入视图添加到任何响应者对象。
UIKit自动设置用户点击的文本字段或文本视图成为第一响应者;应用程序必须使用becomeFirstResponder方法显式设置所有其他第一响应者对象。
响应者链遵循特定的传递路径
如果初始对象(命中测试视图或第一响应者)不处理事件,则UIKit将事件传递给链中的下一个响应者。每个响应者通过调用nextResponder
方法来决定是要处理事件还是将它传递给它自己的下一个响应者。这个过程一直持续到响应者对象处理事件或没有更多响应者。
响应者链序列从iOS检测到事件并将其传递给初始对象(通常是视图)开始。初始视图有首先处理事件的机会。图2-2显示了两种应用程序配置的两种不同的事件传递路径。
图 2-2 iOS响应者链
对于左侧的应用程序,该事件遵循以下路径:
- 初始视图尝试处理事件或消息。如果它无法处理事件,它会将事件传递给它的父视图,因为初始视图不是视图控制器视图层次结构中最顶层的视图。
- 父视图试图处理这个事件。如果父视图不能处理事件,它将事件传递给它的父视图,因为它仍然不是视图层次结构中最顶层的视图。
- 视图控制器的视图层次结构中最顶层的视图尝试处理该事件。如果最上面的视图无法处理该事件,它将该事件传递给其视图控制器。
- 视图控制器尝试处理事件,如果不能,则将事件传递给窗口(window)。
- 如果窗口对象无法处理事件,它将事件传递给单例应用程序(UIApplication)对象。
- 如果应用程序对象无法处理该事件,则会丢弃该事件。
右侧应用程序遵循以下路径:
- 视图将事件传递到其视图控制器的视图层次结构,直到它到达最顶层的视图。
- 最顶层的视图将事件传递给其视图控制器。
- 视图控制器将事件传递给其最顶层视图的父视图。 重复步骤1-3直到事件到达根视图控制器。
- 根视图控制器将事件传递给窗口对象。
- 窗口将事件传递给应用程序对象。