iOS 事件响应

当用户产生一个事件,app就需要对这个事件进行响应。一个事件会经历一条特定的路径直到传递到能够处理它的对象处。一开始,UIApplication的单例将头部的事件从事件队列里面取出来,分发给能够处理它的对象,一般是keywindow。如果是触摸类的事件,那么keywindow首先会尝试将事件传递给触摸发生的视图(可以通过hit-test来找到触摸发生的视图)。如果是动作或者远程控制类的事件,keywindow会传递给first responder进行处理。

所有的事件路径,它的终极目标就是为了找到一个能处理并响应该事件的对象。

hit-testing找到发生触摸事件的视图

看官网上的这张图,

1.触摸发生在视图A内,那么就检查A的子视图B和C。

2.触摸发生在子视图C内,不在子视图B内。那么就去检查视图C的子视图D和E,不再去检查B的子视图。

3.触摸发生在视图E中,不在视图D中,且E没有子视图是包含这个触摸点的,那么E就是我们要找的触摸发生的视图。

hit-test

此处有一个要注意的,用3D的视角来看下面这个图,B是A的子视图,B有超出A区域之外的灰色部分。如果触摸发生在灰色部分,(视图默认的clipsToBounds属性是NO,表示子视图超过的部分不会被裁剪),那么B收不到触摸事件。因为触摸没有发生在A的区域内,所以就不会去检查A的子视图是否能响应触摸。

用代码举个例子

// ViewController.m
#import "ViewController.h"
#import "FatherView.h"
#import "SonView.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    FatherView * fatherView = [[FatherView alloc] initWithFrame:CGRectMake(30, 30, 100, 100)];
    fatherView.backgroundColor = [UIColor orangeColor];
    
    SonView * sonView = [[SonView alloc] initWithFrame:CGRectMake(10, 10, 200, 200)];
    sonView.backgroundColor = [UIColor redColor];
    
    [fatherView setNeedsDisplay];
    [sonView setNeedsDisplay];
    
    [fatherView addSubview:sonView];
    [self.view addSubview:fatherView];
    
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"viewcontroller touchesBegin");
}


// FatherView.m
#import "FatherView.h"

@implementation FatherView
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"fatherview touchesBegin");
}

@end

// SonView.m
@implementation SonView
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"SonView touchesBegin");
}

@end

sample to test cliptobounds when deliver touch event

这个时候,你在子视图即红色视图超过橘色视图的部分触摸一下,打印出来如下。即,SonView和FatherView都没有响应,而是ViewController进行了响应。(照惯性思维,触摸是发生在红色视图上的,应该由红色视图响应,实际上不是。)

viewcontroller respons

如果父视图设置了clipsToBounds = YES,那么它的子视图超过的部分就会被裁减掉。例如下面的代码,效果是这样的。

	FatherView * fatherView = [[FatherView alloc] initWithFrame:CGRectMake(30, 30, 100, 100)];
    fatherView.backgroundColor = [UIColor orangeColor];
    fatherView.clipsToBounds = YES;
    
    SonView * sonView = [[SonView alloc] initWithFrame:CGRectMake(10, 10, 200, 200)];
    sonView.backgroundColor = [UIColor redColor];
    
    
    [fatherView setNeedsDisplay];
    [sonView setNeedsDisplay];
    
    [fatherView addSubview:sonView];
    [self.view addSubview:fatherView];

father view clipstobounds


http://www.jianshu.com/p/c5fee92ddf31

http://smnh.me/hit-testing-in-ios/


responder chain 响应链

能响应事件的对象叫做responder object,UIResponder是这些对象的基类, UIApplicationUIViewControllerUIView都是可以响应事件的。

注意

note

响应链除了响应触摸和动作事件之外,还响应其他东西,下图是从官网上截下来的响应链能处理的所有东西。

responder chain

即其他还有远程控制事件、动作消息(比如按下一个按钮,但按钮的target是nil,那么消息就在响应链上传递)、编辑消息(比如剪切、拷贝、黏贴)、编辑textField或者textView。

特定的响应路径

看一下官网上的图,响应链是这样的。如果initialView没有响应,那么就会层层往父级传递,直至最后传递到UIApplication,如果UIApplication也不能处理它,那么这个消息就被忽略。

responder chain on iOS

userInteractionEnable

当一个view设置userInteractionEnable为NO的时候,它就不会响应用户交互相关的动作了。

那么如果父视图设置了userInteractionEnable=NO,子视图还会接收到触摸吗?


UIResponder

看一下这个类里面的相关方法。

nextResponder,即在响应链中找到下一个响应对象。

isFirstResponder是否是第一响应者

canBecomeFirstResponderbecomeFirstResponder,canResignFirstResponder,resignFirstResponder看名字就知道了。

inputView只有在UITextField或者UITextView的时候才是可赋值的,其他情况下都只是read-only只读。inputViewControllerinputAccessoryView,inputAccessoryViewController同理。

reloadInputViews,当成为first responder的时候更新inputView和accessoryView

响应触摸事件

touch event

最后一个方法touchesEstimatedPropertiesUpdated:是iOS 9.1新加上去的方法。以后再研究。

响应Motion事件

motion event

还有其他相关方法,不一一列举了。

参考

1.https://developer.apple.com/library/ios/documentation/General/Conceptual/Devpedia-CocoaApp/Responder.html

2.UIResponder官方文档

3.视图层级-官方文档

4.事件--官方文档

5.Target-Action--官方文档

6.事件传递链--官方文档

7.iOS Hit-Testing