响应者链工作原理
应用程序使用Responder
对象接收和处理时间,响应者对象是UIResponder
类的任何实例,常见的子类包括UIView
,UIViewController
和UIApplication
.响应者收到原始事件数据,并且必须处理该事件或将其转发给另一个响应者对象,当您的应程序收到一个事件时,UIKit
会自动将该事件指向最合适的响应者对象,称为第一响应者,未处理的事件从响应者传递到活动响应器中的响应者,这是应用程序的响应者对象的动态配置,应用程序中没有单个响应者链,UIKit
定义了将对象从一个响应者传递到下一个响应器的默认规则,但是您可以随时通过覆盖响应者对象中相对应的属性来更改这些规则.下图显示了应用程序的默认响应者链,其界面包含Label
,textField
,button
和两个背景视图,如果label不处理事件,UIKit
会将事件发送到其superView
对象,然后是窗口的根视图,从根视图响应者链转移到拥有该视图的控制器,然后将事件传递给window,如果window
不处理,UIKit
将事件传递给UIApplication
对象,并且可能将该事件传递给AppDelegate
,
对于每种类型的事件,UIKit
指定一个第一响应者,并首先发送事件到这个对象,第一响应者根据事件的类型而有所不同.
- Touch events
第一响应者是发生触摸的视图 - Press events
第一响应者是有焦点的响应者 - Shake-motion events
第一响应者是(UIKit
)指定为第一个响应者的对象 - Remote-control events
第一响应者是(UIKit
)指定为第一个响应者的对象 - Editing menu messages
第一响应者是(UIKit
)指定为第一个响应者的对象
与加速器,陀螺仪和磁力计相关的运动事件不遵循响应者连,Core Motion
将这些事件直接传递给你指定的对象,(https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/iPhoneOSTechOverview/CoreServicesLayer/CoreServicesLayer.html#//apple_ref/doc/uid/TP40007898-CH10-SW27)
当手指触摸到屏幕中,触摸到某一个控件到响应这个事件分为两步:事件的传递与分发,和事件的响应
事件的传递涉及到UIView
中的两个方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
//判断当前点击事件是否存在最优响应者(First Responder)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
//判断当前点击是否在控件的Bounds之内
UIKit使用基于视图的命中测试来确定触摸事件发生的位置。 具体来说,UIKit将触摸位置与视图层次结构中的视图对象的边界进行比较。 UIView的hitTest(_:with :)方法行进视图层次结构,寻找包含指定触摸的最深的子视图。 该视图成为触摸事件的第一响应者
注意如果触摸位置在视图的边界之外,则hitTest(_:with :)方法会忽略该视图及其所有子视图。 因此,当视图的clipsToBounds属性为false时,即使它们包含触摸,也不会返回该视图边界之外的子视图
事件的传递其实就是在事件产生于分发之后如何寻找最优响应的一个过程
你可以修改响应者对象的next属性来更改响应者链,当你修改了此属性时下一个响应者就是你返回的对象,
许多UIKit类已经覆盖此属性并返回特定对象。
UIView
对象,如果这个控制器的View是根视图,那么next responder 就是viewController,否则,就是这个view的父视图-
UIViewController
对象,- 如果该controller的view是window的根视图,那么next responder就是window
- 如果controller是由另一个controller presented的,那么next responder是the presenting Controller.
UIWindow
对象,next responder 就是UIApplication
对象UIApplication
对象,那么 next responder 就是 app delegate ,但前提是appDelegate
是UIResponder
的一个实例,而不是view,viewcontroller或者是app本身.
(以上来自官方文档)
事件的传递过程
1.触碰屏幕产生事件UIEvent
并存入UIApplication
中事件队列中,并在整个视图结构中自下而上进行分发,如下图
2.UIWindow
接收到事件开始进行最优响应视图查询过程(逆序遍历子视图)
3.当到UiviewController
这一层时同样对其视图开始最优响应视图查询,该查询会调用上述提到的两个方法,采用逆序查询也是为了优化查找速度,毕竟后添加的视图易于命中
命中查找流程
1.调用hitTest
方法进行最优响应视图查找
- hidden = YES;
- userInteractionEnabled = NO;
- alpha < 0.01
这三种情况hitTest方法会返回nil ,即当前视图下无最有相应视图,无法响应事件
2.hitTest方法内部调用pointInside方法对点击进行是否在当前视图bounds内进行判断,如果超出bounds范围,则hitTest返回nil,未超出范围继续步骤3
3.对当前视图下的subviews进行逆序步骤1,2查询最优响应者,如果hitTest返回视图,则说明当前视图有最优响应者,可能是self也可能是subview,
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.alpha < 0.01 || !self.userInteractionEnabled || self.hidden) {
return nil;
}
if (![self pointInside:point withEvent:event]) {
return nil;
}
__block UIView *hitView = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) {
hitView = [subview hitTest:point withEvent:event];
if (hitView) {
*stop = YES;
}
}];
return hitView ? : self;
}
总结
- 事件分发与传递:自上而下(
UIApplication
-window
-.....) - 事件响应:自下而上(
view
-superView
-.........)
打印结果: