iOS 事件传递及响应链
记得在几年前写过一篇文章《不规则 Button 点击》研究了一下 iOS 中事件响应的相关知识。
最近刚好碰到了一些点击和手势相关的问题,就详细记录一下 iOS 中事件响应,事件传递以及结合手势一起使用时的情况。
寻找最佳响应者和事件传递
响应者
iOS 系统中,我们使用 UIKit 提供的众多类来搭建页面,其中 UIResponder 类为我们提供了响应事件的能力,我们常用的 UIView、UIViewController 和 UIApplication 都是 UIResponder 的子类,几乎我们常接触的一些控件都是响应者。
UIResponder 提供了以下一些方法,来处理事件:
1 | open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) |
- UITouch
存储用户触摸屏幕时的相关数据,随着触摸的变化,UITouch 数据也会实时更新。 - UIEvent
被传递的事件实例,里面存储了当前需要被响应事件的一些信息。例如,事件类型。
寻找最佳响应者
现在,弄清楚了处理事件的对象了。但是,屏幕上那么多视图堆叠,肯定是需要一套机制,让我们判断这次事件应该由谁先处理,也就是寻找最佳响应者,这个过程也叫做 Hit-Testing。
Hit-Testing 过程主要用到两个方法:
1 | // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system |
接下来,通过一个演示例子来说明具体的流程:
PS: 系统检测到一个触摸事件发生后,会有一系列的处理,这边就忽略掉系统处理的流程,直接进入到开发人员需要关注的地方。
寻找过程
首先,先明确一下视图层级:
点击 A 内部
具体流程说明:
- 首先对 A 进行命中测试,显然触摸点在 A 内部。于是,判断 A 是否有子视图,有则检查,无则返回 A 本身。
- 倒序遍历 A 的子视图(D,C),倒序的原因是,后添加视图,在 subViews 数组的后面,但是后添加的视图,但是在视图层级中处于最上方。
- 对 D 进行命中测试,发现触摸点不在 D 中,说明 D 及其子视图不会是最佳响应者。
- 对 C 进行命中测试,发现触摸点不在 C 中,说明 C 及其子视图不会是最佳响应者。
- 子视图检查完毕。得到结论:触摸点在 A 内部,但不在 A 的任一子视图内。于是,A 就是最佳响应者。
A 🆗 –> D❌ –> C❌ >>> A
点击 C 内部
具体流程说明:
- 首先对 A 进行命中测试,显然触摸点在 A 内部。于是,判断 A 是否有子视图,有则检查,无则返回 A 本身。
- 倒序遍历 A 的子视图(D,C),倒序的原因是,后添加视图,在 subViews 数组的后面,但是后添加的视图,但是在视图层级中处于最上方。
- 对 D 进行命中测试,发现触摸点不在 D 中,说明 D 及其子视图不会是最佳响应者。
- 对 C 进行命中测试,触摸点在 C 中,进行子视图的检查。
- 对 E 进行命中测试,发现触摸点不在 E 中,说明 E 及其子视图不会是最佳响应者。
- 子视图检查完毕。得到结论:触摸点在 A 内部子视图 C 中,但不在 C 的任一子视图内。于是,C 就是最佳响应者。
A 🆗 –> D❌ –> C🆗 –> E❌ >>> C
点击 E 内部
这个就不写了,给看文章的你们,练习一下。
小结
1. Hit-Testing 触发了两次的问题
这个问题苹果官方进行过回复:
Yes, it’s normal. The system may tweak the point being hit tested between the calls. Since hitTest should be a pure function with no side-effects, this should be fine.
2. 视图本身能否接收事件的判断
当视图本身状态设置,符合下方任一时,则视图自身不能响应事件。
- view.isUserInteractionEnabled = false
- view.isHidden = true
- view.alpha <= 0.01
3. 代码实现
这两个方法都是可以被 override 的。所以,如果需求中有一些异性的控件需要特殊处理时,可以针对自己的需求,在这里面做一些修改。
1 | class TestTouchView: UIView { |
传递事件
找到最佳响应者之后,接下来就是事件处理了,最佳响应者拥有优先处理事件的机会,如果该视图无法处理这个事件,那么事件会沿着寻找响应者时的路径反向传递。
以上面章节 点击 C 内部 的例子来说明一下:
- 寻找最佳响应者过程
A 🆗 –> D❌ –> C🆗 –> E❌ >>> C
- 事件传递
去掉打叉的节点,并逆转一下,就得到:
C –> A
再把这个响应链完整一下就是:
C -> A -> UIViewController 对象 -> UIWindow 对象 -> UIApplication 对象 -> App Delegate
找到下一个响应者,其实是通过 UIResponder 的 next 属性得到的,对于 UIView 来说,next 就是其父视图或者是其所属的控制器对象。
1 | open var next: UIResponder? { get } |
这里还是举个例子说明一下。
小结
- 利用 Hit-Testing 找到 最佳响应者。由下往上(从根视图往上寻找)
- 由最佳响应者的 next 属性来确定响应链。
- 事件沿着 响应链 传递。从上往下(从最佳响应者往下寻找)
响应链中的手势处理
官方文档
Gesture recognizers receive touch and press events before their view does. If a view’s gesture recognizers fail to recognize a sequence of touches, UIKit sends the touches to the view. If the view doesn’t handle the touches, UIKit passes them up the responder chain.
文档中 表明了手势识别器在视图之前接收触摸和按下事件。如果视图的手势识别器无法识别一系列触摸,UIKit 会将触摸发送到该视图。如果视图不处理触摸,UIKit 会将它们沿着响应器链向上传递。
传递的是什么呢?其实就是 UITouch 和 UIEvent,在 UITouch 中,有一个属性 gestureRecognizers 存储了触摸时收集到的所有手势,因为 UITouch 是会随着触摸的变化,不断地更新的,所以 gestureRecognizers 会存储响应链上所有节点的手势,并且在自身不断更新的同时,手势识别系统也会判断当前的 UITouch 对象是否符合收集到的手势。
1 | // 手势是否识别成功了,但是状态是否要由 .possible 转到其它活跃状态交给开发者控制。 |
我们还是搞一个例子看看,根据结果再来说明:
- 找到最佳响应者之后,手势系统先判断是否接收触摸和事件。
- UIResponder 的 touches 系列的方法开始触发。
- 手势识别成功了,给响应者发送 touchesCancelled 消息,之后不会收到来自该 UITouch 对象的事件了。
- 手势系统工作。
UIControl 和手势
UIControl 通过 target-action 方式来处理事件,但是 UIControl.Event 的事件是通过其 touches 系列方法来识别的,而手势处理的时机会更早一些。
这里是一个自定义的 TestControl 继承自 UIControl,添加了 .touchUpInside 事件和点击事件的例子:
1 | class TouchEventViewController: UIViewController, UIGestureRecognizerDelegate { |
对于自定义的 UIControl 来说,手势识别的优先级比 UIControl 自身处理事件的优先级高。
如果是这样的话,还有一个问题就是,如果 UIControl 的父视图上存在与自身 controlEvents 一样的手势事件时,会导致 UIControl 的方法无法触发,总会被父视图中的手势给截断。
但是,这仅仅是针对咱们自定义的 UIControl 而言。在实际使用过程中,我们一般都是在使用系统提供了一系列 UIControl 的子类,并不会存在上述的问题。那是因为,苹果官方已经对这种情况做过一些特殊处理。
实际开发中,当我们给一个已经拥有点击手势的视图,添加一个 UIButton 作为子视图,并且给按钮添加点击类型的 target-action 方法,那么当点击按钮时,按钮的 target-action 方法会触发,手势的方法会被忽略。
并且文档中也提到了,如果不想要这种情况发生,那就应当把手势添加到目标控件上(因为手势比控件更早识别到事件,也就是上文提到的给 UIControl 添加了 .touchupInside 方法的例子),这样的话生效的就是手势了。
总结
- 事件传递到 APP 内部时被封装成开发者可见的 UIEvent 对象,先经过 Hit-Testing 寻找最佳响应者,而后将事件传递给最佳响应者,并开始在响应链上的传递。
- UIRespnder、UIGestureRecognizer、UIControl,笼统地讲,事件响应优先级依次递增。