iOS 事件响应链
0x0. 简介
iOS 中的事件类型有Touch event
、Press events
、Shake-motion events
、Remote-control events
,本文暂时只分析最常用的Touch event
。
App 使用 responder 对象 (UIResponder实例) 来接收和处理事件。常见的子类包括 UIView, UIViewController 和 UIApplication。当 responder 对象接收到原始事件数据后,必须处理事件或将其转发给另一个 responder 对象。
🌴
0x1. 查找响应者
事件要想被处理,就必须得有个 responder 来响应它。那么一个事件是如何找到它的 responder 的呢?
hitTest:withEvent:
0x1.1. 这个方法相信大家一定不陌生,UIKit 便是使用这个方法 (点击测试) 来查找触摸事件的响应者。
该方法内部会调用pointInside:withEvent:
方法:
- 如果
pointInside:withEvent:
返回 YES,则会遍历当前视图的所有 subview,对每个 subview 依次调用hitTest:withEvent:
方法。如果 subview 还有 subview,则以此类推,直到找到包含触摸位置的最前面的视图 (即最后 add 的视图) 为止。 - 如果
pointInside:withEvent:
返回 NO,那么hitTest:withEvent:
方法会返回 nil,并且当前视图的 subview 也不会被遍历。
subview 的遍历顺序,是从后往前遍历,即先遍历最后 add 的子视图。当然,并不是所有的 view 都会遍历,如果一个 view 满足下列条件任意一个或多个,将会被忽略。
1. hidden = YES
2. userInteraction = NO
3. alpha < 0.01
通过断点调试,推测出伪代码如下:
static BOOL _UIViewIgnoresTouchEvents(UIView *view) {
if (!view.isUserInteractionEnabled || view.isHidden || view.alpha < 0.01) {
return YES;
}
return NO;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (_UIViewIgnoresTouchEvents(self)) {
return nil;
}
if ([self pointInside:point withEvent:event] == NO) {
return nil;
}
__block UIView *view = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
view = [obj hitTest:point withEvent:event];
if (view) {
*stop = YES;
}
}];
return view ? view : self;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
稍微留意下,会发现虽然 touch 了一次,但是hitTest:withEvent:
却调用了两次,关于这个问题,只找到了这么一个差不多的解释:
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.
🌴
0x2. 处理事件
当找到 responder 对象时,会优先判断是否响应手势。如果 responder 上添加了手势,并且手势代理方法gestureRecognizer:shouldReceiveTouch:
返回 YES,那么就会直接响应手势。
如果没有手势可以响应,那么就会调用touchesBegan:withEvent:
方法。若想自己处理事件,可以重写该方法,重写后,通常不需要调用 super,只需处理事件即可。如果调用了[super touchesBegan:touches withEvent:event];
,则事件将会沿着响应链转发传递。
比如,我们子类化一个 UIButton,重写了touchesBegan:withEvent:
且没有调用 super ,那么当点击 button 时,你会发现添加在 button 上的 action 并不会调用,因为事件默认在touchesBegan:withEvent:
中处理了。
从官方文档中,取一张事件响应链的图片: