我们希望在我们的app中可以动态的响应触摸事件.比如一个触摸可能会发生在屏幕上不同的位置和不同的组件上, 我们需要判断哪个组件响应这个触摸并且了解这个组件是如何接受到触摸事件的.
当一个用户触摸事件发生了, UIKit会创建一个包含需要被处理的事件信息的对象.然后将这个对象放入当前的事件循环队列中,对于触摸事件,这个对象被创建为 UIEvent 对象,对于移动事件这个对象会依赖于你使用的 Framework和你所关心的移动事件.
我们假设这个事件对象为 EventA
EventA会通过一条特殊的路径进行传递, 直到它能被处理为止. 首先 单例的 UIApplication 对象从事件队列中拿到了EventA并且分发这个事件对象. 通常, UIApplication 会将 EventA 分配到创建EventA的 应用key Window 对象中去处理. EventA 是依赖于发生事件的类型, 共有两种类型:
触摸事件(Touch events) 对于触摸事件, Window 对象会尝试将事件传递给触摸发生的view. 这个view被称为 hit-test view. 那么, 找到这个view的过程叫做 hit-testing. 找到view的过程会在下面说明.
摇晃和远程控制事件(Motion and remote control events) 处理这个事件, Window 对象会发出一个 晃动事件 或者 远程控制事件给第一响应者, 如何找到第一响应者,会在下面说明.
那么, 这些事件传递路径最终的目的是找到一个能处理它的对象.所以, UIKit 会将这些事件发送给最适合的对象来处理它们.对于触摸事件, 这个处理对象是 hit-test view, 对于其他的事件, 这个对象是 第一响应者 first responder.
iOS使用 hit-testing 找到被触摸到view. Hit-testing 操作包括检查触摸是否在相关的view的大小之内(bounds), 如果是, 会循环遍历这个view的所有子视图和子视图的子视图, 在所有视图层级中,层级最低的并且包含这个触摸事件的视图就是 hit-test view 当iOS找到这个视图后, 会讲事件对象交给这个视图去处理.
下面有个图示, 我们说明一下如何找到 hit-testing view
假设我们点击个View E 视图, 系统首先调用 hitTest:withEvent:
, 将View A 和 点击坐标点传入,遍历寻找子view, 这时, 找到触摸点在View C上, 继续寻找View C的子View, 然后找到触摸点在View E上, 并且View E 没有子视图了, 所有 方法返回 View E作为 hit-test View, 成为第一响应者.
hitTest:withEvent:
是UIView的方法,它通过给定的 点击坐标和事 CGPoint and UIEvent ,该方法内部调用pointInside:withEvent:
来检查触摸点是否在一个view上,如果返回YES, 说明在view上, 然后hitTest:withEvent:
循环调用pointInside:withEvent:
返回为YES的view.
如果触摸点没有在调用hitTest:withEvent:
view上, 内部调用pointInside:withEvent:
将返回NO, hitTest:withEvent:
将返回nil. 如果一个子视图的hitTest:withEvent:
返回NO, 那么它下面的所有视图层级都讲倍忽略,因为触摸点没有在任何一个它的子视图上,
* 注意: 一个触摸事件在他的生命周期内都与这个hit-test view绑定, 即使触摸点随后移出了view的边界 *
这个 hit-test view 是第一个有机会处理触摸事件的视图, 如果这个view不能处理这个事件, 事件将会沿着view的层级链向上查找第一个能处理他的视图,找到并处理后就停止传递.
许多事件都需要依靠响应链来传递消息. 响应者链是一系列响应者连接起来的序列.它从第一响应者开始到 application对象结束.如果第一响应者无法处理事件, 事件会被传递到响应者链上的下一个响应者处理.
一个响应者是一个能够响应和处理事件的对象. UIResponder 类是所有响应者的基类,它为事件处理和响应者一般的操作定义了一些接口.像UIApplication, UIViewController 和 UIView 的对象都是响应者. 注意, Core Animation layers 不是响应者.
第一响应者是被指定为第一个接收事件的响应者, 通常, 第一响应者是一个UIView 对象. 一个对象要成为第一响应者, 必须做两件事:
canBecomeFirstResponder
为YESbecomeFirstResponder
发来的消息* 注意: 确保一个对象被指定为第一响应者之前, 已经建立了对象或视图. 比如: 你通常调用了 becomeFirstResponder
在 viewDidAppear:
方法中, 如果你尝试指定第一响应者在 viewWillAppear:
方法中, 但是你的响应对象没有建立, 就会导致 becomeFirstResponder
返回 NO*
响应者链还用于下面的事件:
remoteControlReceivedWithEvent:
cut:
copy:
paste:
方法. UIKit 会自动的将被点击的text field 或 text view 设置为第一响应者,我们必须显示的调用 becomeFirstResponder
来设置其他的对象成为第一响应者.
如果初始化的对象-不是hit-test view, 就是第一响应者, 没有处理事件, UIKit 会将事件沿着响者链传递下去. 每个响应者都要决定是否要处理事件或者是通过调用 nextResponder
方法继续传递给下一个响应者. 这个过程一直持续到一个响应者处理了事件或者没有下一个响应者为止.
响应者链开始在iOS发现一个事件并传递给初始化的对象, 通常是一个View. 这个初始化的对象是第一个有机会处理事件. 下图为两种事件传递路径, 取决于不同的视图构造.
对于左边的app, 事件传递如下:
右边的app 传递路径有一点不一样, 但是传递规则遵循下面的尝试算法:
重要提示: 如果你的自定义view来处理 remotecontrolevents,actionmessages,shake-motion events, or editing-menu messages 这些消息或事件, 不要将事件或消息直接通过 nextResponder
转发到响应链中. 而应该是调用父类来实现当前的事件处理, 让UIKit 来处理事件在响应链中的转发