响应者链

来源:互联网 发布:男士双肩包品牌知乎 编辑:程序博客网 时间:2024/05/17 01:29

响应者链 和 事件分发

一、响应者链

当我们看到iOS的一个界面时,iOS的响应者链就已经形成了,它并不是一个对象或实体,它是一条虚拟链条,从当前点击的视图一直连接到程序显示的主Window,再到程序的代理Delegate单例,这个链条就是响应者链。

在UIKit中,所有的视图都有一个父类UIView,而UIView的父类是UIResponder,所有controller的基类UIViewController也UIResponder的子类,所以UIResponder这个类构成了响应者链的基础。通过UIResponder类的实例的下面这个方法,我们可以一直获取整个响应者链。

 - (nullable UIResponder*)nextResponder;

下面是一个简单的例子,通过navigation推出一个ViewController在其中加入一个灰色背景的View,在灰色背景的View中一个紫色背景的View

图1

下面我们在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);});}

对应的结果如下:

图2

从这里我可以的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里打印,就不会有这个问题了。

图3

上图中的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:方法的处理流程如下:

  1. 当在一个视图上调用这个方法时,这个方法首先会调用当前视图的pointInside:withEvent方法,判断触摸是否发生在当前视图内。
  2. 如果pointInside:withEvent返回NO,则hitTest:withEvent:返回nil。
  3. 如果返回YES,说明触摸点在当前视图内,此时会向当前视图的所有子视图发送hitTest:withEvent:方法,调用顺序为从后向前遍历,即后加入的视图先调用。
  4. 如果所有的子视图的hitTest:withEvent:都返回空,则返回self
  5. 如果有子视图的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; }}
0 0
原创粉丝点击