Event Delivery

Introduction

我们在设计APP的时候,想要让APP可以动态的响应事件。比方说,手指触摸到屏幕上的内容的时候,可以动态的判断出哪个对象来响应这个触摸事件,以及这个对象如何接收到这个事件。
在上一篇Event(iOS)中讲解了iOS APP中产生的各种事件,接下来我们来讲这些事件是如何传递到对应的事件接收者的。

事件传递路径概述

当用户生成一个事件时,UIkit会生成一个包含处理事件必要信息的事件对象。然后把这个事件对象加入到处于活跃状态的APP事件队列中。在触摸事件中,这个对象是一系列触摸打包好的UIEvent 对象,在运动事件中,这个对象取决于你使用的框架和你感兴趣的运动类型。
事件会沿着固定的路径进行,直到它被分配给能处理它的对象。

  1. 首先,UIApplication 对象会从事件队列中取出事件,将它分发出去;
  2. 一般,它会分发给APP的主window对象;
  3. 主window对象再把事件分给原始对象处理。

原始对象:

什么是原始对象,取决于事件的类型:

•    Touch events. 触摸事件中,window会首先尝试把事件分发给触摸产生的view。这个view被称为**hit-test view**。找到这个hit-test view的方法是调用**hit-testing**方法,这个方法会返回触摸事件发生在哪个view上面。
•    Motion and remote control events.这一类的事件,window会发送摇晃运动或者远程控制事件给第一响应者处理。(第一响应者会在下面叙述)

总之,这些事件路径最终的目的都是为了找到能处理事件的对象。因此,UIkit会首先把这个事件发送给最适合处理它的对象。在触摸事件中,这个对象是一个hit-test view,在其他事件中,这个对象是第一响应者。下面介绍如何定义hit-test view和第一响应者。

Hit-Testing 方法

iOS使用hit-testing方法找到触摸的view。hit-testing过程会检查触摸点在不在任何相关view的bounds范围内。如果在,会接着检查在不在这个view的子view的bounds内。在iOS决定了hit-test view之后,会把这个触摸事件发送给这个view来处理。
举例阐述:
如图2-1,假设用户触摸了viewE,iOS找hit-test view的顺序将是:

  1. 触摸在viewA 的bounds内,所以检查A的子控件B 和C;
  2. 触摸不造B的bounds中,但是在viewC的bounds中,所以检查D和E;
  3. 触摸不在D中,但是在E中。viewE是view等级结构中最底层的view,所以称为hit-test view。

Figure 2-1 Hit-testing returns the subview that was touched
Mou icon

hit-testing 过程-pointInside:withEvent:

hitTest:withEvent: 这个方法根据传的point和event对象找到hit-test view。调用 hitTest:withEvent: 这个方法之前,会先调用这个view的 pointInside:withEvent: 这个方法. 如果这个方法返回yes,表示传给hitTest:withEvent: 这个方法的point在这个view的bounds范围内.然后会递归调用每个返回yes的子view的hitTest:withEvent: 这个方法直到最后。
如果pointInside:withEvent: 这个方法返回NO,表示传给hitTest:withEvent: 这个方法的point不在这个view的bounds中,所以hitTest:withEvent:这个方法会返回nil。
如果一个子view的pointInside:withEvent:这个方法返回NO,这个view的整个view层级分支都会被忽略掉,因为如果触摸不在这个view上面,那么也不可能在这个view的其他子控件上了。

这就意味着,如果触摸点不在这个view的父view上面,那么这个view也不可能接收到触摸事件了。如果一个子view的clipsToBounds这个属性为NO(默认状态),将会发生这种情况。

注意:一个触摸对象在它整个是生命周期中是和它的hit-test view相关联的,即时它最后离开了这个view。

事件分发依赖于响应者链条

很多类型的事件分发依赖于响应者链条。响应者链条是由一系列响应者对象链接起来的。开始于第一响应者,结束于application对象。如果第一响应者不能处理该事件,这个事件将会沿着响应者链条分给给下一个响应者。
响应者是指能够接收和处理事件的对象。
UIresponder 这个类是所有响应者对象的基类,它为事件处理和普通的响应行为定义编程接口。UIApplication UIViewController UIView 等类的实例对象都属于响应者。注意 core animation layer 不属于响应者。

第一响应者

第一响应者是指定来第一个接收事件的对象。一般,第一响应者是一个view对象。成为第一响应者需要做下面两件事:

1    重写 canBecomeFirstResponder 这个方法,返回YES。
2    接收 becomeFirstResponder 这个消息,如果有必要,可以自己给自己发送这个消息。

注意:在让一个对象成为第一响应者之前,要确保你的APP已经建立了它的对象图表。例如,一般你可以在viewDidAppear: 这个方法中调用becomeFirstResponder 这个方法。如果你在viewWillAppear: 这个方法里面设定第一响应者,你的对象图还没有建立,becomeFirstResponder 这个方法会返回NO。

响应者链条应用

依赖于响应者链条的不仅仅有事件对象,响应者链条还应用于下面的情况:

  1. Touch events触摸事件. 如果hit-test view不能处理触摸事件,这个事件将会被沿着响应者链条传递下去。
  2. Motion events运动事件. 要想使用UIkit处理摇晃运动事件,第一响应者必须实现motionBegan:withEvent: 或者 motionEnded:withEvent: 这两个 UIResponder 类声明的方法。
  3. Remote control events远程控制事件. 第一响应者必须实现remoteControlReceivedWithEvent: 这个UIResponder 类声明的方法。
  4. Action messages操作消息. 如果用户操作一个按钮或者开关的时候,这个操作方法的target对象为空,那么这个操作消息将通过以这个操作对象作为第一响应者的响应者链条来发送。
  5. Editing-menu messages编辑菜单消息. 当用户点击了编辑菜单中的命令(剪切,复制,粘贴等),iOS通过响应者链条找到实现了必要方法的响应者
  6. Text editing文字编辑. 当用户点击文字编辑框或者一个文字编辑页面,这个页面会自动成为第一响应者。默认情况下会弹出键盘,然后text field 或者 text view成为文字编辑的焦点。你也可是自定义键盘来代替系统键盘。你也可以添加一个自定义input view到任何响应者上面。UIkit 会在用户点击text field 或者 text view的时候,自动把它们设置为第一响应者。APP必须明确通过becomeFirstResponder 这个方法才能设置其他响应者为第一响应者。点击查看更多详细信息

重点: 如果你使用自定义view使用UIkit处理远程控制事件,行为消息,摇晃事件或者编辑菜单消息,不要直接把这个事件或者消息转发给响应者链条里的nextResponder ,而是要调用父类的当前事件处理方法实现,让UIkit来处理响应者链条的遍历。