响应者链 和 事件分发
一、响应者链
当我们看到iOS的一个界面时,iOS的响应者链就已经形成了,它并不是一个对象或实体,它是一条虚拟链条,从当前点击的视图一直连接到程序显示的主Window,再到程序的代理Delegate单例,这个链条就是响应者链。
在UIKit中,所有的视图都有一个父类UIView,而UIView的父类是UIResponder,所有controller的基类UIViewController也UIResponder的子类,所以UIResponder这个类构成了响应者链的基础。通过UIResponder类的实例的下面这个方法,我们可以一直获取整个响应者链。
- (nullable UIResponder*)nextResponder;
下面是一个简单的例子,通过navigation推出一个ViewController在其中加入一个灰色背景的View,在灰色背景的View中一个紫色背景的View
下面我们在ViewController的viewDidLoad方法中查看一下nextResponder
- (void)viewDidLoad {
[super viewDidLoad];
ResponderTestView *backView = [[ResponderTestView alloc]initWithFrame:CGRectMake(20, 100, [UIScreen mainScreen].bounds.size.width - 20 * 2, 200)];
backView.backgroundColor = [UIColor grayColor];
[self.view addSubview:backView];
ResponderTestButton *button = [[ResponderTestButton alloc]initWithFrame:CGRectMake(50, 30, 100, 40)];
button.backgroundColor = [UIColor purpleColor];
[button addTarget:self action:@selector(clickButton) forControlEvents:UIControlEventTouchUpInside];
[backView addSubview:button];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"1 %@",button.nextResponder);
NSLog(@"2 %@",button.nextResponder.nextResponder);
NSLog(@"3 %@",button.nextResponder.nextResponder.nextResponder);
NSLog(@"4 %@",button.nextResponder.nextResponder.nextResponder.nextResponder);
NSLog(@"5 %@",button.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder);
NSLog(@"6 %@",button.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder);
NSLog(@"7 %@",button.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder);
NSLog(@"8 %@",button.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder);
NSLog(@"9 %@",button.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder);
NSLog(@"10 %@",button.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder);
NSLog(@"11 %@",button.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder);
});
}
对应的结果如下:
从这里我可以的1到12就是一条完整的响应者链,13是空是因为AppDelegate作为相应者链的终点,不再有nextResponder,如果在AppDelegate还没有被处理的话,这个事件就会被丢弃。一个view的nextResponder是它的父视图,controller的view 即controller的self.view的nextResponder是controller,controller的nextResponder是承载它的controller的self.view,根controller的nextResponder是UIWindow,UIWindow的nextResponder是UIApplication,UIApplication的nextResponder是AppDelegate。
咦,上面的打印为什么在延时里面进行,因为viewDidLoad调用的比较早,这时可能当前viewController的子响应者链还没有加入到根响应者链中去,如果我们不用延时,那么结果可能会是下图这样的。当然如果不在viewDidLoad里打印,就不会有这个问题了。
上图中的1到13,数字小的有优先处理事件的权利,当1不处理事件时,2开始处理事件,如果2不处理,则传到3,以此类推。
二、事件分发
iOS系统检测到手指触摸操作时会将其放入到当前活动Application的事件队列中,UIApplication会从事件队列中取出触摸事件并传递给key window处理,window对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图,即需要将触摸事件传递给其处理的视图,称之为hit-test view。
window对象会首先在顶级view上调用hitTest:withEvent: 这个方法会在顶级视图的子视图中调用pointInside:withEvent:,如果pointInside:withEvent:返回YES,则这个子视图的hitTest:withEvenyt:方法返回当前子视图, 子视图会对子视图的所有子视图调用pointInSide;withEvent:方法,以此逐级调用,直到找到当前touch的view。
hitTest:withEvent:会忽略hidden=YES、userInterationEnabled=NO、alpha<0.01的视图。
hitTest:withEvent:方法的处理流程如下:
- 当在一个视图上调用这个方法时,这个方法首先会调用当前视图的pointInside:withEvent方法,判断触摸是否发生在当前视图内。
- 如果pointInside:withEvent返回NO,则hitTest:withEvent:返回nil。
- 如果返回YES,说明触摸点在当前视图内,此时会向当前视图的所有子视图发送hitTest:withEvent:方法,调用顺序为从后向前遍历,即后加入的视图先调用。
- 如果所有的子视图的hitTest:withEvent:都返回空,则返回self
- 如果有子视图的hitTest:withEvent:返回非空,则返回此视图,处理结束。
hitTest:withEvent:的简单应用
改变Button的点击区域,重写button的hitTest:withEvent:方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { CGRect rect = CGRectInset(self.bounds, -50, -50); if (CGRectContainsPoint(rect, point)) { return self; } return nil; }
也可以重写pointInside:withEvent:来达到这个效果
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect rect = CGRectInset(self.bounds, -50, -50);
if (CGRectContainsPoint(rect, point)) {
return YES;
}
return NO;
}
当上方有一个遮罩时,如果这时我们想让遮罩下方的view响应事件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *view = [super hitTest:point withEvent:event]; if (view == self) { return nil; } else { return view; } }