iOS进阶- 响应者链

点击时间传递过程

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.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- 事件拦截

iOS进阶- 响应者链_第1张图片
image.png

如上图假如我们想当我点击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- 事件转发

image.png

如上图,区域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 * 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;
}

你可能感兴趣的:(iOS进阶- 响应者链)