响应者链

响应者链 和 事件分发
一、响应者链

当我们看到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;
     }
    
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值