ios——事件传递与响应者链

一、事件分类

事件是发送到应用程序用于通知用户操作的对象。 在iOS中,事件可以采取多种形式:多点触摸事件,运动事件和用于控制多媒体的事件。 这最后一种类型的事件被称为遥控事件或者远程控制事件,因为它可以源自外部附件。而在我们开发过程中最常用的就是多点触摸事件。

二、事件传递

当用户生成的事件发生时,UIKit创建一个包含处理事件所需信息的事件对象。 然后它将事件对象放置在活动应用程序的事件队列中。 对于触摸事件,该对象是在UIEvent对象中打包的一组触摸(UIEvent中包含了所有UITouch信息)。 对于运动事件,事件对象因您使用的框架和您感兴趣的运动事件类型而异。

事件沿着特定路径传递,直到它被传递到可以处理它的对象。 首先,单例UIApplication对象从队列的顶部获取一个事件并分发处理。 通常,它将事件发送到应用程序的key window对象,该对象将事件传递到初始对象(initial object)进行处理。 初始对象取决于事件的类型。

  • 触摸事件:对于触摸事件,窗口对象首先尝试将事件传递到发生触摸的视图。 该视图称为命中测试视图(hit-test view)。 找到命中测试视图(hit-test view)的过程称为命中测试(hit-testing),这在Hit-Testing返回触摸发生的视图中描述。

  • 运动和遥控事件:对于这些事件,窗口对象将摇动或远程控制事件发送到第一响应者以进行处理。 第一响应者在响应者链由响应者对象组成中描述。

这些事件路径的最终目标是找到一个可以处理和响应事件的对象。 因此,UIKit首先将事件发送到最适合处理事件的对象。 对于触摸事件,该对象是命中测试视图(hit-test view),对于其他事件,该对象是第一个响应者。

{\large\text{作者:坤坤同学 链接:https://www.jianshu.com/p/847432c2cb3b 来源: 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。}}

三、命中测试

一根手指触摸屏幕时会创建一个UITouch对象,最终生成UIEvent对象,并通过sendEvent:函数发送给UIWindowkeyWindow)。

  1. UIApplication接收到事件,将事件传递给keyWindow
  2. keyWindow遍历subViewshitTest:withEvent:方法,找到点击区域内合适的视图来处理事件。
  3. UIView的子视图也会遍历其subViewshitTest:withEvent:方法,以此类推。
  4. 直到找到点击区域内,且处于最上方的视图,将视图逐步返回给UIApplication
  5. 在查找第一响应者的过程中,已经形成了一个响应者链。
  6. 应用程序会先调用第一响应者处理事件。
  7. 如果第一响应者不能处理事件,则调用其nextResponder方法,一直找响应者链中能处理该事件的对象。
  8. 然后交给UIApplication后,最后交给UIApplicationDelegate,仍然没有能处理该事件的对象,则该事件被废弃。
  • 这里涉及两条链:
    Hit-Testing链,由系统向命中view传递UIKit –> active app's event queue –> window –> root view –>......–>lowest view
    响应链,由命中view向系统传递initial view –> super view –> .....–> view controller –> window –> Application –> AppDelegate

举例说明:
1.如果点击UITextField后其会成为第一响应者。
2.如果textField未处理事件,则会将事件传递给下一级响应者链,也就是其父视图。
3.父视图未处理事件则继续向下传递,也就是UIViewControllerView
4.如果控制器的View未处理事件,则会交给控制器处理。
5.控制器未处理则会交给UIWindow
6.然后会交给UIApplication
7.最后交给UIApplicationDelegate,如果其未处理则丢弃事件。

案例说明,假设用户触摸下图中的View E。 iOS通过按照此顺序检查子视图来查找命中测试视图(hit-test view):

  1. 触摸在View A的边界内,因此它检查子视图View BView C.

  2. 触摸不在View B的界限内,但它在View C的界限内,因此它检查子视图View DView E.

  3. 触摸不在View D的界限内,但它在View E的界限内。

View E是视图层级中包含触摸的最低的视图,因此它成为命中测试视图(hit-test view)。

//模拟代码:
- (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;
}
四、知识点应用
  1. 调用hitTest,获取到被点击的视图,也就是第一响应者:
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
    想让指定视图来响应事件,不再遍历子视图传递事件,可以通过重写hitTest方法。

  2. hitTest方法内部会通过调用pointInside,来判断点击区域是否在视图上,是则返回YES,不是则返回NO
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
    通过重写pointInside方法,可以将有效点击区域扩大。

  3. 另外,应用程序通过响应者来接收和处理事件(能够响应事件的对象都是UIResponder的子类对象,例如UIViewUIViewControllerUIApplication等)。当事件来到时,系统会将事件传递给合适的响应者,并且将其成为第一响应者。
    第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIRespondernextResponder决定,可以通过重写该属性来决定传递规则。当一个事件到来时,第一响应者没有接收消息,则顺着响应者链向后传递。

如果命中测试视图不能处理这个事件,就会往上传递
五、注意点

在遍历视图时,忽略以下三种情况的视图,如果视图具有以下特征则忽略:

  1. 视图的hidden等于YES
  2. 视图的alpha小于等于0.01
  3. 视图的userInteractionEnabledNO

但是视图的背景颜色是clearColor,并不在忽略范围内。

六、优先级

事件到来后先会执行hitTestpointInside操作,通过这两个方法找到第一响应者。当找到第一响应者并将其返回给UIApplication后,UIApplication会向第一响应者派发事件,并且遍历整个响应者链。开始会执行响应者链中的touches系列方法。会先执行touchesBegantouchesMoved方法,如果响应者链能够继续响应事件,则执行touchesEnded方法表示事件完成。如果响应者链中有能够处理当前事件的手势,则将事件交给手势处理,调用touchesCancelled方法将响应者链打断。
如果UIButton(所有继承自UIControl类)是第一响应者,则直接由UIApplication派发事件,不通过响应者链派发。如果其不能处理事件,则交给手势处理或响应者链传递。

  • 代码验证
  1. 自定义TestView重写touches系列方法:
@implementation TestView

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesBegan TextView");
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesMoved TextView");
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesEnded TextView");
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesCancelled TextView");
}
@end
//点击结果
touchesBegan TextView
touchesEnded TextView

TestView或者其父控件添加UITapGestureRecognizer点击手势后:

//点击结果
touchesBegan TextView
tap
touchesCancelled TextView

view添加单击手势之后,原来的touchesEnded方法就无效了,继而执行touchesCancelled

  1. 自定义TestButton重写Tracking系列方法,并添加点击方法:
@implementation TestButton

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"beginTracking");
    return YES;
}

- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"continueTracking");
    return YES;
}

- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"endTracking");
}

- (void)cancelTrackingWithEvent:(UIEvent *)event {
    NSLog(@"cancelTracking");
}
//点击效果
beginTracking
endTracking
buttonToClick

给其父控件添加UITapGestureRecognizer点击手势后:

//点击效果
beginTracking
endTracking
buttonToClick
  • 优先级:系统的UIControl > 手势 > 自定义的UIControl

如果给TestButton添加UITapGestureRecognizer点击手势后:

//点击效果
beginTracking
tap
cancelTracking
  • 补充:最后响应的途径便是sendAction分发event到一个对象去处理:
按钮
手势
七、补充点
传递与响应
  • source1runloop用来处理mach port传来的系统事件的,source0是用来处理用户事件的。在之前Runloop执行流程中提到过source1source0

推荐参考:https://mp.weixin.qq.com/s/kkWWCb1Zy4d-lPRdPUoVHg
(文章部分来自此参考)

你可能感兴趣的:(ios——事件传递与响应者链)