点击时间传递过程
UI 事件 = UIEvent + 寻找最佳的事件接受者 + 事件响应
当我们在界面发生一个点击手势,我们知道系统系统会生成一个UIEvent事件放到事件队列里面,然后Application从事件队列取出事件接着是后面的寻找响应。当找到最佳的事件接受者后,然后会进行事件冒泡找到事件处理对象即事件响应对象。总体分3步,接下来我们进行讲解:
第一步: UIEvent的产生过程
首先由IOKit.framwork 产生一个IOHITEVENT事件并由 SpringBoard 接收,然后SpringBoard 会通过系统内核的mach port 将事件转发给我们的APP进程,然后触发由App在Runloop注册的Source1来处理事件,Source1内部调 IOHITEVENTSYSTERMCLIENTQueueCallBlack,IOHITEVENTSYSTERMCLIENTQueueCallBlack内部回调了 Source0,由此生成这个UIEvent事件并假如事件处理队列, UIApplication然后从事件队列中取出事件进行派发。
第二步: 寻找最佳响应者
2.1 首先连接寻找响应者视图,UIView的两个重要方法
// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
// 返回视图层级中能响应触控点的最深视图
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
// default returns YES if point is in bounds
// 返回视图是否包含指定的某个点
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
2.2 寻找过程
当我们在界面上进行一个点击手势时,系统会生成一个UIEvent事件,并将事件放入事件队列等待处理,UIApplication接收到UIEvent事件后,首先调用UIWindow的 ( hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; )方法, hitTest内部调用pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event(判断当前的触控点在UIWindow里面) 返回YES,UIWindow会遍历它所有的子视图,得到第一个子视图首先判断它完足(可交互、不是隐藏、不是透明、hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;)条件,如果不满足这些条件,返回Nill,直接进入下一个子试图循环;如果满足,进行下一步,判断它是否是否还有子试图,如果有子试图,继续遍历它的子试图,按照当前的流程去判断,如果没有就说明找到响应者视图,将它返回出去。
hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; 内部原理
- 首先判断是否当前视图是否可以响应当前控制点,有四种情况视为不能响应
1.1 当前视图设为hidden = YES ;
1.2 当前的视图设为了 userInteractionEnabled=NO ;
1.3 当前视图的是View.alpha < 0.01;
1.4 当前视图超出了父控件;
模拟 hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; 内部实现
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1.判断下自己能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判断下点在不在当前控件上
if ([self pointInside:point withEvent:event] == NO) return nil; // 点不在当前控件
// 3.从后往前遍历自己的子控件
int count = self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
// 获取子控件
UIView *childView = self.subviews[i];
// 把当前坐标系上的点转换成子控件上的点
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) {
return fitView;
}
}
// 4.如果没有比自己合适的子控件,最合适的view就是自己
return self;
}
一张图就可以:
[图片上传中...(image.png-525732-1558679254716-0)]
第二步: 事件响应
当第二步发现控制点在一个试图里,并且当前试图没有子试图,也是找到在当前响应者链中最底层的响应者试图,并将它返回去,然后会按原始探寻路径一直返回到根试图;
无法响应的情况:
1.Alpha=0、子视图超出父视图的情况、userInteractionEnabled=NO、hidden=YES视图会被忽略,不会调用hitTest
2.父视图被忽略后其所有子视图也会被忽略,所以View3上的button不会有点击反应
3.出现视图无法响应的情况,可以考虑上诉情况来排查问题
使用场景1- 事件拦截

如上图假如我们想当我点击button时候,红色视图来处理,实现思路就是当调用红色区域的 hitTest:(CGPoint)point withEvent:(UIEvent *)event 如果控制点在它里面就直接返回不进行下一步子视图的探寻;
实现方法:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 如果在当前 view 中 直接返回 self 这样自身就成为了第一响应者 subViews 不再能够接受到响应事件
if ([self pointInside:point withEvent:event]) {
return self;
}
return nil;
}
使用场景12- 事件转发

如上图,区域a超出了父视图,区域b在父视图,正常我们点击区域a,它是没有反应的,因为当区域a调用 pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event 因为点击坐标不再在的里面导致返回NO 。解决思路就是 重写区域a的 hitTest:(CGPoint)point withEvent:(UIEvent *)event,如果区域a的 pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event 返回NO,我们将当前控制点的坐标系设置为区域a,然后重新调用探寻区域a,这是空点点就在区域a里返回YES既可以响应啦:
(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 触摸点在视图范围内 则交由父类处理
if ([self pointInside:point withEvent:event]) {
return [super hitTest:point withEvent:event];
}
// 如果触摸点不在范围内 而在子视图范围内依旧返回子视图
NSArray<UIView *> * superViews = self.subviews;
// 倒序 从最上面的一个视图开始查找
for (NSUInteger i = superViews.count; i > 0; i--) {
UIView * subview = superViews[i - 1];
// 转换坐标系 使坐标基于子视图
CGPoint newPoint = [self convertPoint:point toView:subview];
// 得到子视图 hitTest 方法返回的值
UIView * view = [subview hitTest:newPoint withEvent:event];
// 如果子视图返回一个view 就直接返回 不在继续遍历
if (view) {
return view;
}
}
return nil;
}
网友评论