前言
在 iOS 中,常见的事件有:触摸事件、加速计事件、远程控制事件等。在这里我们主要讨论触摸事件,对于触摸事件的传递流程,我们需要先了解响应者对象和响应者链是什么,这样子才可以更加清晰的认识事件的传递流程和响应流程,然后再利用这些知识点来解决业务需求。
响应者对象
只有响应者对象才可以接收处理事件,在 iOS 中,只有 UIResponder 及其子类称为响应者对象,平时我们的 UIApplication
、UIViewController
、UIView
都是继承自 UIResponder,所以它们都是响应者对象,可以接收处理事件。对于 CALayer
不是继承自 UIResponder 的,这就是为什么 CALayer
没有响应事件的能力。
对于触摸事件,UIResponder 提供了下面方法来处理触摸事件:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
触摸事件的产生和传递
用户触摸屏幕产生事件,系统将事件交给 UIApplication 管理分发,UIApplication 将事件分发给 KeyWindow,然后再寻找出一个最合适的响应者来响应这个事件。
如何寻找出最合适的响应者,主要依靠下面两个函数:
//返回最合适的 View 来响应事件
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
// 判断当前的触摸点是否在 View 中
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
这里引用 初探 iOS 事件分发机制 解释:
Hit-Test View:当用户与触摸屏产生交互时,硬件就会探测到物理接触并且通知操作系统。操作系统就会创建相应的事件,并将其传递给当前正在运行的应用程序的事件队列。然后这个事件会被事件循环传递给优先响应对象,既 Hit-Test View
Hit-Testing:Hit-Test View 就是事件被触发时和用户交互的对象,寻找 Hit-Test View 的过程就叫做 Hit-Testing
现在我们知道事件的传递是靠上面两个方法来寻找最合适的响应者,找到响应者后会调用响应者的 touch
函数进行事件处理,大概流程是:
产生触摸事件 -> UIApplication 事件队列 -> [UIWindow hitTest:withEvent:] -> 返回更合适的view -> [子控件 hitTest:withEvent:] -> 返回最合适的view -> [Application sendEvent] -> 调用最合适 view 的 touch 函数处理事件
响应者链及事件响应流程
页面的控件具有层级关系,响应者也会有层级关系,由响应者组成层级关系称为响应者链。UIResponder 中有个 nextResponder
属性返回下一个响应者对象。当一个响应者接收到事件但是不能处理时候,会交给下一个响应者去处理,最终要是谁都处理不了该事件,则会抛弃这个事件。
对于响应者链,可以参考下图:
事件传递和事件响应区别
事件传递是从父控件到子控件传递,从上到下;事件响应是顺着响应者链向上传递(从子控件到父控件),从下到上。
实战-子视图和父视图同时处理事件
子视图重写 touch
函数来处理事件,然后再调用 super touch 将事件传递给父视图:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
//子视图处理该事件
//调用 super 让父视图也处理该事件
[super touchesBegan:touches withEvent:event];
}
实战-扩大一个视图的点击范围
可以通过 pointInside
函数,将该视图周围的触摸事件也当成自己的事件处理:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect relativeFrame = self.bounds;
UIEdgeInsets hitTestEdgeInsets = UIEdgeInsetsMake(-15, -15, -15, -15);
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
实战-深层级 View 通讯
假设控制器上面添加 AView,AView 添加了 BView,BView 又添加了 CView 等,在 CView 产生了一个事件需要让控制器来处理,这个时候如果用 Block、Delegate、Notification 都会比较麻烦,这个时候可以通过响应者链,将消息传递上去。
- 首先我们为 UIResponder 写个分类方法,类似 Router 方法
- 只需要在 CView 中调用该方法,让控制器去监听该方法就 OK 了
具体代码实现:
//UIResponder 分类实现
/**
发送一个路由器消息, 对eventName感兴趣的 UIResponsder 可以对消息进行处理
@param eventName 发生的事件名称
@param userInfo 传递消息时, 携带的数据, 数据传递过程中, 会有新的数据添加
*/
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSObject *)userInfo {
[[self nextResponder] routerEventWithName:eventName userInfo:userInfo];
}
//CView 调用
[self routerEventWithName:@"CViewEvent" userInfo:nil];
//控制器监听
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSObject *)userInfo {
NSLog(@"%s eventName:%@",__func__,eventName);
}
实战-HitTest 大概实现
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) {
return nil;
}
BOOL inside = [self pointInside:point withEvent:event];
if (inside) {
NSArray *subViews = self.subviews;
// 对子视图从上向下找
for (NSInteger i = subViews.count - 1; i >= 0; i--) {
UIView *subView = subViews[i];
CGPoint insidePoint = [self convertPoint:point toView:subView];
UIView *hitView = [subView hitTest:insidePoint withEvent:event];
if (hitView) {
return hitView;
}
}
return self;
}
return nil;
}
总结
这篇我们主要了解了响应者对象是什么,事件的传递流程以及事件响应流程。了解了这些知识后,还是对我们平时开发有所帮助的。
对于更加详细的介绍,可以看看后面的博客链接。
网友评论