dailyLearning -- 响应者链

  • 响应者对象介绍
  • 什么是响应者链
  • 事件响应流程(事件的产生和传递)
  • 怎么寻找最合适的 view
  • 应用

runLoop 的介绍中, 说到了,runLoop 在事件响应的应用,

苹果注册了一个Source1(基于mach port的) 用来接收系统事件, 其回调函数为 _IOHIDEventSystenClientQueueCallback();

当一个硬件事件 (触摸/锁屏/摇晃等) 发生后, 首先由IOKit.framework 生成一个IOHIDEvent事件并由SpringBoard接收, 这个过程的详细情况可以参考这里; SpringBoard 只接收按键(锁屏/静音等), 触摸,加速,接近传感器等几种 event, 随后通过 mach port 转发给需要的 App 进程; 随后触发 App 注册 苹果注册的那个 Source1 就会触发回调, 并调用_UIApplicationHandleEventQueu() 进行应用内部的分发;

_UIApplicationHandleEventQueue()会把IOHIDEvent 处理并包装成 UIEvent 进行处理或者分发, 其中包括识别UIGesture / 处理屏幕旋转 / 发送给 UIWindow 等; 通常事件比如 UIButton 点击, touchsBegin / Move / End / Cancel 事件都是在这个回调中完成的;

所以当我们 触摸手机屏幕时, 系统会将这一操作封装成一个UIEvent 对象, 放到 runLoop 的事件队列里面, UIApplication从事件队列取出事件, 然后找到该事件的第一响应者处理该事件, 这里主要介绍事件的产生和传递 — 响应者链;

一、响应者对象介绍

响应者对象是什么?
响应者对象是一个能够响应和处理事件的对象; UIResponder 是所有响应者对象的基类, 继承自 UIResponder 的对象称为响应者对象; UIApplication, UIWindow, UIViewController 和所有继承自 UIView 的 UIKit 类都直接或间接继承自 UIResponder;

UIResponder 一般响应一下几种事件: 触摸事件(touch handling), 点按事件(press handling), 加速事件 和 远程事件;

//触摸事件(touch handling)
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches NS_AVAILABLE_IOS(9_1);
//点按事件(press handling) NS_AVAILABLE_IOS(9_0)
- (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
//加速事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
//远程控制事件
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);

二、什么是响应者链
响应者链: 由多个响应者组合起来的链条, 就叫做响应者链; 他表示了每个响应者之间的联系, 并且可以使得一个事件可选择多个对象处理;

dailyLearning -- 响应者链_第1张图片

当触摸了 initial view 时:

  1. 第一响应者就是 initial view , 即 initial view 首先响应 touchesBegan:withEvent: 方法, 接着传递给 橘黄色的 view;
  2. 橘黄色 view 开始响应 touchesBegan:withEvent: 方法, 接着传递给蓝绿色 view;
  3. 蓝绿色 view 响应 touchesBegan:withEvent: 方法, 接着传递给控制器的 view;
  4. 控制器 view 响应 touchesBegan:withEvent: 方法, 控制器传递给窗口 window;
  1. 窗口 window 再传递给 UIApplication 处理该事件;

如果上述响应者都不处理该事件, 那么事件被丢弃;

三、事件响应流程(事件的产生和传递)

当一个触摸事件产生的时候, 程序是如何找到第一响应者呢


dailyLearning -- 响应者链_第2张图片

当点击屏幕时会产生一个触摸事件, 消息循环(runLoop) 会接收到触摸事件, 将事件包装成 UIEvent 对象, 放到主循环的消息队列里, UIApplication 会从消息队列里取出事件, 分发下去;

首先传给 UIWindow, UIWindow 通过 hitTest:withEvent: 方法找到此次触摸事件初始点所在的视图,找到这个视图之后,就会调用该视图的 touchesBegan:withEvent:方法来处理此事件;

iOS系统检测到手指触摸(Touch)操作时会将其放入当前活动Application的事件队列,UIApplication会从事件队列中取出触摸事件并传递给key window(当前接收用户事件的窗口)处理,window对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,称之为hit-test view

hitTest:withEvent: 查找响应者过程

dailyLearning -- 响应者链_第3张图片

图片中view等级

    [ViewA addSubview:ViewB];
    [ViewA addSubview:ViewC];
    [ViewB addSubview:ViewD];
    [ViewB addSubview:ViewE];

点击 ViewE:

  1. A 是 UIWindow 的根视图, 首先对 A 进行hitTest:withEvent:
  2. pointInside:withEvent:方法判断用户点击是否在 A 的范围;
  3. 遍历 A 的子视图 B 和 C, 从后向前遍历;
  • 因此, 先查看 C , 调用 C 的 hitTest:withEvent:方法, pointInside:withEvent:判断用户点击是否在 C 的范围内, 不在返回 NO, C 对应的hitTest:withEvent: 返回 nil;
  • 再查看 B, 调用 B 的 hitTest:withEvent: 方法, pointInside:withEvent: 判断用户点击是否在 B 的范围内, 在返回 YES;
  • 再遍历 B 的子视图 D 和 E, 从后向前遍历;
  • 先查看 E, 调用 E 的 hitTest:withEvent: 方法,pointInside:withEvent: 判断用户点击是否在 E 的范围内, 在返回 YES; E 没有子视图, 因此 E 对应的 hitTest:withEvent: 方法返回 E, 再往前回溯, 就是 B 的 hitTest:withEvent: 返回 E, A 的 hitTest:withEvent: 返回 E;

至此, 点击事件的第一响应者找到了;

如果 hitTest:withEvent: 找到了第一响应者, 但 view 没有处理该事件, 那么事件会沿着响应者链向上传递 -> 父视图 -> 视图控制器 -> UIWindow -> UIApplication , 如果传递到响应链最顶级还没有处理事件, 就丢弃该事件;

dailyLearning -- 响应者链_第4张图片

注意: 控件不能响应的情况,

  1. userInteractionEnabled = NO;
  2. hidden = YES;
  3. 视图透明度 alpha <= 0.01;
  4. 子视图超出父视图区域;

子视图超出父视图, 不响应的原因:
因为父视图的pointInside:withEvent: 方法返回 NO, 就不会遍历子视图了, 可以重写pointInside:withEvent: 方法解决此问题;

四、怎么寻找最合适的 view

hitTest:withEvent:

// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

事件传递给 window 窗口或者控件后, 就调用hitTest:withEvent: 方法寻找更合适的 view, 如果控件是合适的 view, 则在子控件再调用 hitTest:withEvent:查看子控件是不是合适的 view, 一直遍历,直到找到合适的 view, 或者废弃事件;

// 因为所有的视图类都是继承BaseView
- (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.从后往前遍历自己的子控件
    NSInteger count = self.subviews.count;
    for (NSInteger 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) { // 寻找到最合适的view
            
            return fitView;
        }
    }
    // 循环结束,表示没有比自己更合适的view
    return self;
}

pointInside:withEvent:

// 判断一个点是否落在范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

这个函数的用处是判断当前的点击或者触摸事件的点是否在当前的view中。

它被hitTest:withEvent:调用,通过对每个子视图调用pointInside:withEvent:决定最终哪个视图来响应此事件。如果pointInside:withEvent:返回YES,然后子视图的继承树就会被遍历(遍历顺序中最先响应的为:与用户最接近的那个视图。 it starts from the top-level subview),即子视图的子视图继续调用递归这个函数,直到找到可以响应的子视图(这个子视图的 hitTest:withEvent: 会返回self,而不是nil);否则,视图的继承树就会被忽略。

当我们需要重写某个UIView的继承类UIViewInherit的时候,如果需要重写 hitTest:withEvent:方法,就会出现是否调用[super hitTest:withEvent:]方法的疑问?究竟是否需要都是看具体需求,这里只是说明调与不调的效果。

如果不调用,那么重写的方法 hitTest:withEvent: 只会调用重写后的代码,根据所重写的代码返回self或nil,如果返回self那么你的这个UIViewInherit类会接受你的按键,然后调用touches系列方法;否则返回nil那么传递给 UIViewInherit 类的按键到此为止,它不接受它的父view给它的按键,即不会调用touches系列方法。这时,pointInside:withEvent: 几乎没有作用。

如果调用,那么[super hitTest:withEvent:]方法首先是根据pointInside:withEvent:的返回值决定是否递归调用所有子View的hitTest:withEvent:方法。对于子 View 的 hitTest:withEvent:方法调用也是一样的过程,这样一直递归下去,直到最先找到的某个递归层次上的子 View 的 hitTest:withEvent: 方法返回非 nil,这时候,调用即结束,最终会调用这个子 View 的 touches 系列方法。

如果我们不想让某个视图响应事件,只需要重载 pointInside:withEvent:方法,让此方法返回NO就行了。不过从这里,还是不能了解到hitTest:WithEvent的方法的用途

五、应用

在实际开发中, 可能会遇到自定义 tabBar, 中间有凸起按钮的情况, 如图:

dailyLearning -- 响应者链_第5张图片

如何做到点击按钮而不会触发页面?
这里就需要用到 hitTest:withEvent:来处理;

一般我们在子类化的 UITabBar 中, 重写 hitTest:withEvent: 方法,

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    if (self.clipsToBounds || self.hidden || (self.alpha == 0.f)) {
        return nil;
    }
    
    UIView *result = [super hitTest:point withEvent:event];

    //如果发生在 tabBar 里面直接返回
    if (result) {
        return result;
    }
    //这里遍历哪些超出部分, 通用写法
    for (UIView *subview in self.subviews) {
        
        //把这个坐标从 tabBar 的坐标系转为 subView 的坐标系
        CGPoint subPoint = [subview convertPoint:point fromView:self];
        result = [subview hitTest:subPoint withEvent:event];
        
        //如果事件发生在 subview 里, 就返回
        if (result) {
            return result;
        }
    }
    return nil;
}

或者

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    //判断当前手指是否点击到中间按钮上,如果是,则响应按钮点击,其他则系统处理
    //首先判断当前View是否被隐藏了,隐藏了就不需要处理了
    if (self.isHidden == NO) {
        
        //将当前tabbar的触摸点转换坐标系,转换到中间按钮的身上,生成一个新的点
        CGPoint newP = [self convertPoint:point toView:self.centerBtn];
        
        //判断如果这个新的点是在中间按钮身上,那么处理点击事件最合适的view就是中间按钮
        //self.centerBtn 为大按钮
        if ([self.centerBtn pointInside:newP withEvent:event]) {
            return self.centerBtn;
        }
    }
    
    return [super hitTest:point withEvent:event];
}

以上 hitTest:withEvent: 的两种实现都可以达到点击 tabBar 外部触发按钮事件的目的;

你可能感兴趣的:(dailyLearning -- 响应者链)