本篇主要讲解iOS事件传递的整个过程,大部分内容翻译自Apple Developer Guide,原文链接
当一个用户事件产生的时候,UIKit 会创建一个事件对象来描述这个用户事件。然后它会将该事件对象放进UIApplication对象所维护的事件队列中。对于触摸事件而言,产生的事件对象便是一个包含UIEvent的集合(NSSet)对象。
一个事件会朝着特定的路径进行传递直到遇到一个可以处理它的对象为止。首先,UIApplication对象会从事件队列的栈顶拿到事件,并且通过分发下去的方式处理该事件。一般情况下,UIApplication发送该事件到app的主窗口对象(key window object),主窗口对象会根据事件类型(Touch event、Motion and remote control events)选择一个相应的对象,将事件传递给它。
- Touch events. 窗口对象首先会尝试将事件传递给事件发生所在的view对象。这个view对象我们称之为 hit-test view。寻找这个hit-test view的过程叫做hit-testing(定位Touch事件发生在哪个view上)。
- Motion and remote control events. 就这些事件而言,窗口对象会将 shaking-motion 或者 remote control event发送给响应者链条的第一个响应者(the first responder)去处理。
事件沿着响应者链条向上传递,其最终目标是发现一个能够响应和处理该事件的对象。因此,UIKit首先将事件传递给一个最适合处理它的对象。就Touch events来说,该对象就是hit-test view,对于其它事件而言,该对象是第一个响应者。下面的章节介绍hit-test view和第一响应者是怎么确定的。
通过hit-test找到触摸的View
当用户触摸屏幕的时候,iOS会使用 hit-testing去找到用户触摸的view对象。hit-testing机制会在所有相关view中检查触摸是否发生在哪一个view的范围内中,如果找到了,那么通过递归的方式继续检查这个view的子view们。当递归结束的时候,处于view层级结构中最下面的那个包含触摸点的view就是 hit-test view。在iOS确定了 hit-test view之后,会将触摸事件传递给它寻求处理。
图2-1举个例子,假设用户触摸了图2-1中的 view E。iOS通过下面的步骤来检查subviews,最终确定hit-test view:
- 触摸点在 view A里面,所以检查它的子控件 B 和 C。
- 触摸点不在 view B里面,但是在 view C里面,所以检查它的子控件 D 和 E。
- 触摸点不在 view D里面,但是在 view E里面。view E是包含该触摸点的最底层 view,所以它就是 hit-test view.
传递一个CGPoint 和 UIEvent对象给UIView 的 hitTest:withEvent: 方法,它会返回 hit-test view对象。hitTest:withEvent: 方法开始的时候会调用自身的 pointInside:withEvent: 方法。如果传递给 hitTest:withEvent: 的点在view对象的范围内,pointInside:withEvent: 会返回YES。如果返回YES,那么父view的 hitTest:withEvent: 会继续通过递归的方式调用子view的 hitTest:withEvent 方法。
如果传递给hitTest:withEvent:的点不在根view的范围内,那么调用 pointInside:withEvent: 会返回NO,这个点就被忽略,然后 hitTest:withEvent: 会返回nil。如果一个子view返回NO,那么从该子view到它的所有子view的递归分支会被忽略,因为如果一个触摸点不在子view的范围内,那么同样的,这个触摸点不会在该子view的所有子view范围内。这意味着,当触摸的点所在的范围是在父view之外,那么子view是无法接收到触摸事件的,因为事件接收的充要条件是触摸点必须同时处在父view和子view的范围内。这种情况发生在子view的大小超过了父view的大小并且子view的clipsToBounds属性值为NO,并且触摸点超过父view的范围,但是位于子view的范围内的时候。
注意:触摸事件对象的整个生命周期都会和它的 hit-test view关联,即使触摸最后超出了这个hit-test view的范围
hit-test view作为第一个可能处理touch event的对象,如果它不能处理这个事件,那么事件会沿着响应者链条往上传递给上一个响应者,直到系统找到一个可以处理这个事件的对象为止。
响应者链条是由响应者对象构成的
许多类型的事件都依赖响应者链条进行事件传递。响应者链条是一连串连接在一起的响应者对象。它从第一响应者开始到UIApplication对象结束。如果第一响应者不能处理事件,系统会将事件传递给响应者链条的下一个响应者。
响应者对象是一个能够响应和处理事件的对象。它们有一个共同点就是都是继承自UIResponder,UIResponder的程序接口不仅定义了事件处理方式,还有响应者的一些共同行为。UIApplication,UIViewController和UIView都是响应者,这意味着UIView与其所有子类和大多数主要控制器对象都是响应者,注意 Core Animation layers 不是响应者。
第一响应者被设计作为第一个接收事件的对象,一般而言,第一响应者是一个view对象。一个对象通过下面的两个步骤成为第一响应者:
- 覆盖 canBecomeFirstResponder方法并且返回YES。
- 接收到一个 becomeFirstResponder 消息,如果有需要,一个对象可以给自己发送这个消息。
注意:在让一个对象成为第一响应者之前,确保你的app已经渲染好它的视图,举个例子,我们一般在viewDidAppear:方法中调用becomeFirstResponder方法,如果我们尝试在viewWillAppear: 方法中调用,由于我们的视图还没有渲染到屏幕上,所以 becomeFirstResponder 方法会返回NO。
事件并不是唯一一种依赖响应者链条进行传递的对象。响应者链条被用在下面的情况中:
- Touch events. 如果 hit-test view不能处理触摸事件,那么事件会顺着响应者链条从 hit-test view 开始传递上去。
- Motion events. 为了让UIKit处理 shake-motion 事件,第一响应者必须覆盖UIRespnder的 motionBegan:withEvent: 或者 motionEnded:withEvent: 方法。
- Remote control events. 为了处理 remote control 事件,第一响应者必须实现UIResponder的 remoteControlReceivedWithEvent: 方法
- Action messages. 当用户对一个UIControl对象进行操作的时候,比如一个button或者switch,如果没有为该对象指定target和action method,那么一个Action message方法会从第一响应者开始顺着响应者链传递出去,这个第一响应者可以是这个UIControl对象本身。
- Editing-menu messages.当用户使用了编辑菜单的一个指令选项的时候,iOS使用响应者链条去找一个实现了对应必要方法(例如cut:,copy: 和 paste: )的对象。
- Text editing. 当用户触摸一个text field 或者是 text view的时候,这个view自动成为第一响应者。默认的,这个时候虚拟键盘会出现并且text field或text view会变成编辑状态。如果需要,你可以使用一个自定义的输入视图代替键盘。同样的,你可以为任何响应者对象添加一个自定义的输入视图(通过重定义inputView属性为readwrite并且赋值)。
在触摸text field和text view的时候,UIKit自动设置它们为第一响应者;如果想让其它UIResponder成为第一响应者,我们必须在代码中设置调用该对象的 becomeFirstRespnder 方法。
响应者链条的事件传递路径是遵循特定的规则形成的
如果最初的对象不是hit-test view或者第一响应者没有处理事件,UIKit会将事件传递给链条中的下一个响应者。每一个响应者都可以决定是去处理这个事件,还是通过调用 nextResponder 方法将事件传递给下一个响应者。这个过程会持续进行下去,直到找到一个能处理事件的响应者或者到UIApplication对象结束。
在iOS检测到一个事件并且将它传递给初始对象(一般是一个view)的时候,响应者链条开始有序地运作。初始view是第一个可能处理响应事件的对象。图2-2展示在两种界面视图结构下的两条事件传递路径。一个app的事件传递路径依赖于它的视图结构,不同的视图结构可能有不同的事件传递路径,但是所有事件传递路径都是有迹可循的。
图2-2左边的app,它的事件传递过程如下:
- initial view 尝试去处理事件或者消息。如果它不能处理事件,它将事件传递给它的父控件,因为initial view 不是它所在控制器的直接视图,所以是将事件往上抛给父控件而不是控制器对象。
- 它的父控件尝试去处理事件。如果父控件不能处理事件,继续将事件传递给它的父控件,因为它也不是控制器的直接视图。
- 控制器的直接视图(controller.view)是所在控制器视图中层级最高的,如果这个视图还是无法处理事件,那么它会将事件抛给控制器。
- 控制器尝试去处理事件,如果不能处理事件,它会将事件抛给window对象。
- 如果window对象无法处理事件,window将事件抛给UIApplication对象。
- 如果UIApplication对象无法处理事件,那么就销毁这个事件。
右边的app,它的事件传递过程明显和第左边的不同,但是所有事件传递都有如下规则:
- 一个view顺着控制器视图的层级结构传递事件,直到到达控制器的直接视图,也就是最顶层的视图。
- 顶层视图将事件传递给它所在的控制器。
- 控制器传递事件给顶层视图的父控件。
步骤 1-3 重复直到到达根控制器。 - 根控制器将事件抛给window对象。
- window对象将事件抛给UIApplication对象。
注意:当你在处理remote control events,action messages, shake-motion events 或者 editing-menu mesages的时候,如果想通过响应者链条传递事件,不要直接调用nextResponder,而是通过调用父类实现好事件处理方法,让UIKit去做事件传递。
网友评论