事件传递:响应链
开发者设计APP时,通常都想要对一些事件进行动态响应。比如,触摸事件可以发生在屏幕上的许多不同的对象上,开发者就需要确定到底想要哪个对象对哪个事件进行响应,并理解该对象是如何接收该事件的。
当用户触摸事件发生时,UIKit框架就会为此创建一个事件对象,该对象自身就包含了能够处理该事件对象所必要的信息。然后UIKit框架将此对象列入active app的事件队列。对于触摸事件,事件对象就是组装的一系列UIEvent对象。对于动作事件,所对应的事件对象因开发者所使用的框架、开发者感兴趣的动作事件类型不同而异。
每一个事件都会沿着特定的路径传递下去,直到某个对象能处理为止。首先,单例对象UIApplication会从事件队列的顶端取出来一个事件并进行派遣,以便开始处理。通常,该事件会被派送给app的主窗体(window)对象,然后由此window对象将事件传递给最初事件发生所在的“初始对象”进行处理,这个初始对象是什么,就依赖于该事件的类型:
- Touch Event(触摸事件)。对于触摸事件,主窗体对象首先尝试将事件传递给事件发生所在的视图view对象。该视图对象view就是所谓的hit-test视图对象。这个寻找hit-test视图对象的过程被称作hit-testing,在章节“Hit-Testing返回触摸事件发生所在的视图对象”有介绍。
- Motion and remote control events(运动和远程控制事件)。对于这些事件,主窗体对象会将摇晃事件或者远程控制事件发送给第一响应器(the first responder)进行处理。第一处理器在章节“响应器链由多个响应器对象组成”。
事件对象路径的最终目标,就是找到能够处理事件并进行响应的对象。因此,UIKit先会将事件发送给最适合处理该事件的对象。对于触摸事件,该对象就是hit-test视图对象,对于其他的事件,该对象就是第一响应器。以下部分会更详细地对hit-test视图对象和所确定的第一响应器进行解释。
1.Hit-Testing返回“发生触摸事件”所在的视图对象
iOS系统使用Hit-Testing去查找到底触摸事件发生在哪个视图对象上。Hit-Testing先检查触摸对象所在的位置是否在对应任意屏幕上的视图对象的区域范围内。如果在的话,就开始对此视图对象的子视图对象进行同样的检查。视图树中最底层那个包含此触摸点位置的视图对象,就是要查找的hit-test视图对象。iOS一旦确定hit-test视图对象,就会把触摸事件传递给它进行处理。
举个例子,假设用户触摸了视图E,如图2-1所示。iOS就会按照以下顺序对子视图进行检查来查找hit-test视图:
- 触摸点在视图A的区域范围内,然后开始检查子视图B和C
- 触摸点不在B的范围而在C的范围,于是就开始检查D和E视图
- 触摸点不在D的范围而在E的范围,而E视图是视图树最底层的并包含触摸点的视图对象,所以E就成为了hit-test视图。
hitTest:withEvent: 方法会返回给定的CGPoint和UIEvent所在的hit-test视图对象。hitTest:withEvent:方法会先调用pointInside: withEvent:方法。如果传入hitTest:withEvent:的CGPoint点对象位于视图对象的区域范围内,pointInside:withEvent:返回值就是YES,然后,该hitTest:withEvent:就会依次在返回YES的子视图对象上调用hitTest:withEvent:。
如果传入hitTest:withEvent:的点不在视图对象的范围内,第一次调用pointInside:withEvent:就会返回NO,这个点就被忽略掉了, hitTest: withEvent: 就返回nil。如果子视图返回NO,那么整个视图树的分支都会被忽略掉,因为如果子视图不包含这个点,那子视图的子视图就更不会包含这些点了。这也就意味着,如果父视图都不包含某个触摸事件的点,子视图即使包含了这个点,也不会接收到此触摸事件,因为只有父视图的区域范围包含了触摸事件发生的位置点,事件才会被继续向子视图传递(即触摸点在 子视图 超出 父视图的frame 部分里)。如果子视图的clipsToBounds属性被设置为NO(亦即子视图超出父视图的部分不会被切割掉,也就是说子视图会有一部分不处在父视图的范围内),这种情况是会发生的。
备注:触摸对象UITouch在其生命周期内会和hit-test视图对象一直关联在一起,即使UITouch在后续的时间里移动并离开该视图对象的范围。
hit-test视图对象拥有最先对触摸事件进行处理的机会,如果hit-test视图对象无法处理该事件,事件对象就会沿着响应器的视图链(参见“响应器链由多个响应器对象组成”)向上传递,直到找到最适合处理该事件的对象为止。
2. 响应器链由多个响应器对象组成
许多类型的事件都依赖于响应器链(responder chain)进行事件传递。响应器链就是一系列的相关联的响应器对象。如果第一个响应器无法处理事件,响应器就会将事件对象传递给响应器链的下一个响应器对象。
一个响应器对象就是一个能够对事件进行处理和响应的实体对象。UIResponder类是所有响应器对象的基类,不仅定义了事件处理的编程接口,同时还定义了通用的响应器行为。★ UIApplication、UIViewController、UIView类的实体对象都是响应器,也就意味着,所有的视图对象和关键控制器对象都是响应器对象。注意Core Animation layers不是响应器。
第一响应器被指定第一个接收事件。通常来讲,第一响应器是一个视图view对象。通过做两件事,一个对象就★ 变成第一响应器:
1.重写canBecomeFirstResponder使其返回YES;
2.接收becomeFirstResponder消息。如果有必要,对象本身可以自己发送此消息。
★ 备注:再将某个对象赋值为第一响应器之前,一定要确保APP已经建立好了对象图谱。比如,通常应该在重写的★ viewDidAppear: 方法中调用becomeFirstResponder方法,但是如果写在了★ viewWillAppear里面,此时因为对象图谱还没有建立起来, becomeFirstResponder 的返回值就NO了。
也不仅仅只是事件对象依赖于响应器链,响应器链可以被用于处理以下所有对象:
- Touch Events(触摸事件)。如果hit-test视图对象无法处理触摸事件,事件就会从hit-test视图沿着响应链网上传递,直到找到合适的处理该事件的对象。
- Motion Events(运动事件)。要使用UIKit处理“摇动”(shake-motion)事件,第一响应器就必须实现方法motionBegan:withEvent:或者motionEnded:withEvent:之一,具体请参见“使用UIEvent检测摇动事件”章节。
- Remote Control Events(远程控制事件)。要对远程控制事件进行处理,第一响应器必须实现基类UIResponder的remoteControlReceivedWithEvent:方法。
- Action messages(动作消息)。当用户操作了某个控件,如按钮button、switch,对应的动作方法的目标是nil,该消息会从以控件视图对象为开始的响应器链被发送出去。
- Editing-menu messages(编辑菜单消息)。当用户点击了编辑菜单的指令,iOS系统就会使用响应器链去查找到对应实现了必要处理方法(如cut:,copy:以及paste:)的对象。要获得更多信息,请参阅章节“显示和管理编辑菜单”和样例代码项目CopyPasteTile。
- Text Editing(文本编辑)。当用户点击某个文本区域(UITextField)或者文本视图(UITextView)时,对应的视图就会成为第一响应器。默认情况下,虚拟键盘会弹出来,而且对应的UITextField或者UITextView就会被选中并变成正在编辑状态。如果开发者觉得合适的话,可以使用自定义的视图来代替默认的软键盘作为用户的输入区域视图。要获取更多信息,请参见章节“自定义数据输入视图”。
当用户点击某个 UITextField或者UITextView的时候,UIKit会自动把将对应的对象设置为第一响应器。对于其他的第一响应器,App必须使用becomeFirstResponder方法显示地进行设置。
3.响应器链的特定传递路径
如果(事件发生所在的)初始对象(要么是hit-test视图,要么是第一响应器)无法对事件进行处理,UIKit就会把事件传递给响应器链的下一个响应器对象。每个响应器对象都可以决定是自己进行事件处理,还是将事件通过方法nextResponder的调用,传递给下一个事件响应器。此过程一直进行下去,直到找到了处理该事件的对象,或者到达了响应器链的最后一个响应器了。
响应器链开始于iOS检测到事件并将其传递到(事件发生所在的)初始对象,通常来讲这个对象是一个视图对象view。初始视图对