事件传递和响应链

前言

看关于这方面的文章基本没有能涉及到UIGestureRecognizers相关的文章,所以决定写这样一篇文章。也是个人第一篇文章,若有什么不对请及时指正。
本文主要经过一些实际测试来便于你们理解。算法

正文

事件传递和响应链流程图

  • IOKit.framework 为系统内核的库
  • SpringBoard.app 至关于手机的桌面
  • Source1 主要接收系统的消息
  • Source0 - UIApplication - UIWindow
  • 从window开始系统会调用hitTest:withEvent:pointInside来找到最优响应者,具体过程可参考下图
    hitTest
    • 好比咱们在self.view 上依次添加view一、view二、view3(3个view是同级关系),那么系统用hitTest以及pointInside时会先从view3开始便利,若是pointInside返回YES就继续遍历view3的subviews(若是view3没有子视图,那么会返回view3),若是pointInside返回NO就开始遍历view2。反序遍历,最后一个添加的subview开始。也算是一种算法优化。后面会具体介绍hitTest的内部实现和具体使用场景。
  • UITouch会给gestureRecognizers和最优响应者也就是hitTestView发送消息
    • 默认view会走其touchBegan:withEvent:等方法,当gestureRecognizers找到识别的gestureRecognizer后,将会独自占有该touch,即会调用其余gestureRecognizer和hitTest view的touchCancelled:withEvent:方法,而且它们再也不收到该touche事件,也就不会走响应链流程。下面会具体阐述UIContol和UIScrollView和其子类与手势之间的冲突和关系。
  • 当该事件响应完毕,主线程的Runloop开始睡眠,等待下一个事件。

1.hitTest:withEvent:和pointInside

1.1 hitTest:withEvent:和pointInside 演练

  • 测试hitTest和pointInside执行过程app

    GSGrayView *grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)];
    [self.view addSubview:grayView];
    
    GSRedView *redView = [[GSRedView alloc] initWithFrame:CGRectMake(0, 0, grayView.bounds.size.width / 2, grayView.bounds.size.height / 3)];
    [grayView addSubview:redView];
    
    GSBlueView *blueView = [[GSBlueView alloc] initWithFrame:CGRectMake(grayView.bounds.size.width/2, grayView.bounds.size.height * 2/3, grayView.bounds.size.width/2, grayView.bounds.size.height/3)];
    
    // blueView.userInteractionEnabled = NO;
    // blueView.hidden = YES;
    // blueView.alpha = 0.1;//0.0;
    [grayView addSubview:blueView];
    
    GSYellowView *yellowView = [[GSYellowView alloc] initWithFrame:CGRectMake(CGRectGetMinX(grayView.frame), CGRectGetMaxY(grayView.frame) + 20, grayView.bounds.size.width, 100)];
    [self.view addSubview:yellowView];

    hitTest测试

    点击redView:
    yellowView -> grayView -> blueView -> redView
  • 当点击redView时,由于yellowView和grayView同级,yellowView比grayView后添加,因此先打印yellowView,因为触摸点不在yellowView中所以打印grayView,而后遍历grayView的subViews分别打印blueView和redView。
  • 当hitTest返回nil时,也不会打印pointInside。所以能够得出pointInside是在hitTest后面执行的。
  • 当view的userInteractionEnabled为NO、hidden为YES或alpha<=0.1时,也不会打印pointInside方法。所以能够推断出在hitTest方法内部会判断若是这些条件一个成立则会返回nil,也不会调用pointInside方法。
  • 若是在grayView的hitTest返回[super hitTest:point event:event],则会执行gery.subviews的遍历(subviews 的 hitTest 与 pointInside),grayView的pointInside是判断触摸点是否在grayView的bounds内,grayView的hitTest是判断是否须要遍历他的subviews.
  • pointInside只是在执行hitTest时,会在hitTest内部调用的一个方法。也就是说pointInside是hitTest的辅助方法。
  • hitTest是一个递归函数ide

    1.2 hitTest:withEvent:内部实现代码还原

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"-----%@",self.nextResponder.class);
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil;
    //判断点在不在这个视图里
    if ([self pointInside:point withEvent:event]) {
        //在这个视图 遍历该视图的子视图
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            //转换坐标到子视图
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            //递归调用hitTest:withEvent继续判断
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                //在这里打印self.class能够看到递归返回的顺序。
                return hitTestView;
            }
        }
        //这里就是该视图没有子视图了 点在该视图中,因此直接返回自己,上面的hitTestView就是这个。
        NSLog(@"命中的view:%@",self.class);
        return self;
    }
    //不在这个视图直接返回nil
    return nil;
}

1.3 pointInside运用:增大热区范围

  • 在开发过程当中不免会遇到须要增大UIButton等的热区范围,假如UIButton的布局不容许修改,那么就须要用到pointInside来增大UIButton的点击热区范围。具体实现代码以下:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    
    NSLog(@"%@ -- pointInside",self.class);
    CGRect bounds = self.bounds;
    //若原热区小于200x200,则放大热区,不然保持原大小不变
    //通常热区范围为40x40 ,此处200是为了便于检测
    CGFloat widthDelta = MAX(200 - bounds.size.width, 0);
    CGFloat heightDelta = MAX(200 - bounds.size.height, 0);
    bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
    return CGRectContainsPoint(bounds, point);
    
}
  • 也就是说若是button的size小于200*200,则点击button相对中心位置上下左右各100的范围内即便超出button,也能够响应点击事件

2.响应链

2.1 响应链的组成

respondChain

还用上面那个栗子:
点击redView:
redview -> grayView -> viewController -> ...

由于只实现到controller的touches事件方法所以只打印到Controller。函数

  • 响应链是经过nextResponder属性组成的一个链表。
    • 点击的view有 superView,nextResponder就是superView;
    • view.nextResponder.nextResponder是viewController 或者是 view.superView. view
    • view.nextResponder.nextResponder.nextResponder是 UIWindow (非严谨,便于理解)
    • view.nextResponder.nextResponder.nextResponder. nextResponder是UIApplication、UIAppdelate、直到nil (非严谨,便于理解)
  • touch事件就是根据响应链的关系来层层调用(咱们重写touch 要记得 super 调用,否则响应链会中断)。
  • 好比咱们监听self.view的touch事件,也是由于subviews的touch都在同一个响应链里。

2.2 UIControl阻断响应链

把上面栗子中的grayView替换成一个Button:oop

GSExpandButton *expandButton = [[GSExpandButton alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)];
    expandButton.backgroundColor = [UIColor lightGrayColor];
    [expandButton setTitle:@"点我啊" forState:UIControlStateNormal];
    [expandButton addTarget:self action:@selector(expandButtonClick:) forControlEvents:UIControlEventTouchDown];
    [self.view addSubview:expandButton];
    
    self.redView = [[GSRedView alloc] initWithFrame:CGRectMake(0, 0, expandButton.bounds.size.width / 2, expandButton.bounds.size.height / 3)];
    [expandButton addSubview:self.redView];
    
    self.blueView = [[GSBlueView alloc] initWithFrame:CGRectMake(expandButton.bounds.size.width/2, expandButton.bounds.size.height * 2/3, expandButton.bounds.size.width/2, expandButton.bounds.size.height/3)];
    
    //    blueView.userInteractionEnabled = NO;
    //    blueView.hidden = YES;
    //    blueView.alpha = 0.1;//0.0;
    [expandButton addSubview:self.blueView];
    
    self.yellowView = [[GSYellowView alloc] initWithFrame:CGRectMake(CGRectGetMinX(expandButton.frame), CGRectGetMaxY(expandButton.frame) + 20, expandButton.bounds.size.width, 100)];
    [self.view addSubview:self.yellowView];

点击redView:
redview -> expandButton
布局

  • 虽然点击redView,虽然button的touches事件方法也走了可是依然不会响应button的target的action方法,只是会传递到button而已,由于最佳响应着依然是redView。
  • 从上面测试结果能够看出,UIControl会阻断响应链的传递,也就是说在响应UIContol的touches事件时并不会调用nextResponder的对应的方法。
  • 经过在Button子类中重写touches的方法,发现若是不调用super的touches对应的方法则不会响应点击事件。由此能够大体推断出UIControl其子类响应点击原理大体为:根据添加target:action:时设置的UIControlEvents,在touches的合适方法调用target的action方法。

2.3UIScrollView阻断响应链

self.grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth, kScreenHeight * 3 / 4)];
    [self.view addSubview:self.grayView];
    
    self.tableView = [[GSTableView alloc] initWithFrame:CGRectMake(0, 20, kScreenWidth, self.grayView.bounds.size.height / 2)];
    self.tableView.dataSource = self;
    self.tableView.backgroundColor = [UIColor darkGrayColor];
    self.tableView.delegate = self;
    [self.grayView addSubview:self.tableView];
    
    self.redView = [[GSRedView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth/2, self.tableView.bounds.size.height/2)];
    [self.tableView addSubview:self.redView];

点击redview
redview -> tableView
测试

  • 从上面测试结果能够得出,UIScrollView也会阻断响应链,也就是说在响应UIScrollView自身对touch的处理方式并不会调用nextResponder对应的方法。
  • 经过重写tableView子类的touches方法,发现若是不调用super的touches对应的方法则不会走tableview:didSelectRowAtIndexPath:方法。由此能够大体推断出UIScrollView其子类是在其touches方法中处理点击事件的。

3.手势

3.1手势的探索以及和touch事件的关系

在上面栗子中的view增长gestureRecognizer:优化

- (void)addGesture {
    GSGrayGestureRecognizer *grayGesture = [[GSGrayGestureRecognizer alloc] initWithTarget:self action:@selector(grayViewClick:)];
    [self.grayView addGestureRecognizer:grayGesture];
    
    GSRedGestureRecognizer *redGesture = [[GSRedGestureRecognizer alloc] initWithTarget:self action:@selector(redViewClick:)];
    [self.redView addGestureRecognizer:redGesture];
    
    GSBlueGestureRecognizer *blueGesture = [[GSBlueGestureRecognizer alloc] initWithTarget:self action:@selector(blueViewClick:)];
    [self.blueView addGestureRecognizer:blueGesture];
}

点击redView
打印结果以下图所示:
ui

  • 当经过hitTest和pointInside找到最优响应者后,会给gestureRecognizers和相应的view同时发送touchBegin消息,若是找到合适gestureRecognizer则会独有该touches,即调用view的touheCancel消息,接着有gestreRecognizer来响应事件。
  • 上面为默认状况下手势和touches之间的关系,其实咱们能够经过gestureRecognizer的属性来控制它们之间的一些关系。
// default is YES. causes touchesCancelled:withEvent: or pressesCancelled:withEvent: to be sent to the view for all touches or presses recognized as part of this gesture immediately before the action method is called.
@property(nonatomic) BOOL cancelsTouchesInView; 

// default is NO.  causes all touch or press events to be delivered to the target view only after this gesture has failed recognition. set to YES to prevent views from processing any touches or presses that may be recognized as part of this gesture      
@property(nonatomic) BOOL delaysTouchesBegan;         

 // default is YES. causes touchesEnded or pressesEnded events to be delivered to the target view only after this gesture has failed recognition. this ensures that a touch or press that is part of the gesture can be cancelled if the gesture is recognized
@property(nonatomic) BOOL delaysTouchesEnded;
  • cancelsTouchesInView:默认为YES。表示当手势识别成功后,取消最佳响应者对象对于事件的响应,并再也不向最佳响应者发送事件。若设置为No,则表示在手势识别器识别成功后仍然向最佳响应者发送事件,最佳响应者仍响应事件。
  • delaysTouchesBegan:默认为NO,即在手势识别器识别手势期间,触摸对象状态发生变化时,都会发送给最佳响应者,若设置成yes,则在识别手势期间,触摸状态发生变化时不会发送给最佳响应者。
  • delaysTouchesEnded:默认为NO。默认状况下当手势识别器未能识别手势时,若此时触摸已经结束,则会当即通知Application发送状态为end的touch事件给最佳响应者以调用 touchesEnded:withEvent: 结束事件响应;若设置为YES,则会在手势识别失败时,延迟一小段时间(0.15s)再调用响应者的 touchesEnded:withEvent:。

3.2手势和UIControl的关系

  • 上面已经说了UIContol会阻断响应链。那么咱们再来进一步探索UIControl的阻断和手势之间的关系。
// button在上面
    self.grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)];
    GSGrayGestureRecognizer *graygesture = [[GSGrayGestureRecognizer alloc] initWithTarget:self action:@selector(grayViewClick:)];
    [self.grayView addGestureRecognizer:graygesture];
    [self.view addSubview:self.grayView];
    
    GSExpandButton *expandButton = [[GSExpandButton alloc] initWithFrame:CGRectMake(30, kTopHeight, 100, 100)];
    expandButton.backgroundColor = [UIColor redColor];
    [expandButton setTitle:@"点我啊" forState:UIControlStateNormal];
    [expandButton addTarget:self action:@selector(expandButtonClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.grayView addSubview:expandButton];

点击button
this

  • 从该栗子中能够看出即便下层view添加收拾依然会响应按钮的点击事件。

  • 由此能够猜想缘由:
    1. UIControl及其子类会阻断响应链。(后面验证是错误的)
    2. UIControl及其子类为最优响应者时会优先处理它们的事件。(后面验证成功)
    • 验证猜想一:
    • 有手势的view上增长一个阻断响应链的view
    self.grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)];
    GSGrayGestureRecognizer *graygesture = [[GSGrayGestureRecognizer alloc] initWithTarget:self action:@selector(grayViewClick:)];
    [self.grayView addGestureRecognizer:graygesture];
    [self.view addSubview:self.grayView];
    GSCancelledTouchView *cancelTouchView = [[GSCancelledTouchView alloc] initWithFrame:CGRectMake(30, kTopHeight, 100, 100)];
    [self.grayView addSubview:cancelTouchView];

    点击greenView
    • greenView是一个阻断响应链的view(即从新超类touches方法没用调用超类方法),可是依然响应gestureRecognizer的target:action:方法,而且调用touches事件的toucesCancelled的方法。所以猜想1是错误的。
    • 验证猜想二:
    • 有收拾的view上增长一个button,button上增长一个view
    // 验证不取消button的touches事件猜想二
    self.grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)];
    GSGrayGestureRecognizer *graygesture = [[GSGrayGestureRecognizer alloc] initWithTarget:self action:@selector(grayViewClick:)];
    [self.grayView addGestureRecognizer:graygesture];
    [self.view addSubview:self.grayView];
    
    GSExpandButton *expandButton = [[GSExpandButton alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth/3, 200)];
    expandButton.backgroundColor = [UIColor redColor];
    [expandButton setTitle:@"点我啊" forState:UIControlStateNormal];
    [expandButton addTarget:self action:@selector(expandButtonClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.grayView addSubview:expandButton];
    
    
    self.blueView = [[GSBlueView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    [expandButton addSubview:self.blueView];

    点击blueView
    • 点击blueview虽然expandButton会阻断响应链可是依然会执行在grayview上的手势方法而且会调用touchesCancelled方法,所以能够验证猜测二是正确的。
    • 把grayview上的gestureRecognizer去掉,依然不会响应expandButton上的点击事件,由于最优响应者不是expandButton。
  • UIControl及其子类可以执行点击事件而不是走底层的手势的缘由为:在识别到相应的gestureRecognizer后若是当前的最优响应者是UIControl及其子类而且当前的gestureRecognizer不是UIContol上的手势,则会响应UIControl的target:action:的方法。不然则会响应gestureRecognizer的target:action:的方法。

3.3 手势和UIScrollView的关系

  • UITableView是UIScroll子类的经常使用类,所以拿UITableView来举栗子。
self.grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth, kScreenHeight * 3 / 4)];
    GSGrayGestureRecognizer *grayGesture = [[GSGrayGestureRecognizer alloc] initWithTarget:self action:@selector(grayViewClick:)];
    //    grayGesture.delaysTouchesBegan = YES;
    //    grayGesture.cancelsTouchesInView = NO;
    //    grayGesture.delaysTouchesEnded = YES;
    [self.grayView addGestureRecognizer:grayGesture];
    [self.view addSubview:self.grayView];
    
    self.tableView = [[GSTableView alloc] initWithFrame:CGRectMake(0, 20, kScreenWidth, self.grayView.bounds.size.height / 2)];
    self.tableView.dataSource = self;
    self.tableView.backgroundColor = [UIColor darkGrayColor];
    self.tableView.delegate = self;
    [self.grayView addSubview:self.tableView];


点击tableView
当父控件没有手势时

当父控件有手势时

  • 由上面的例子能够得出当UIScrollView为最优响应者而且父控件没有手势时UIScrollView才能够本身处理点击事件。不然被父控件的gestureRecognizer占有。
  • 从上面结果看出当父控件有手势时UIScrollView的touches方法都不执行,相似于设置delaysTouchesBegan为YES。
  • 虽然UIScrollView及其子类和UIControl及其子类相似均可以阻断响应链,可是当UIScrollView及其子类为最优响应者时,若是父控件中有gestureRecognizer依然会被其占有。
UIScrollView点击穿透解决方案

当UIScrollView为最优响应者父控件有手势时,UIScrollView及其子类的点击代理方法和touchesBegan方法不响应。

解决方法:三种解决方式,我的认为第二种为最优解决方案

  • 能够经过给父控件手势设置cancelsTouchesInView为NO,则会同时响应gestureRecognizer的事件和UIScrollView及其子类的代理方法和touches事件。

  • 给父控件中的手势的代理方法里面作一下判断,当touch的view是咱们须要触发的view的时候,return NO ,这样就不会走手势方法,而去触发这个touch.view这个对象的方法了。

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    if ([NSStringFromClass([touch.view class])    isEqualToString:@"UITableViewCellContentView"]) {
        return NO;
    }
    return YES;
}
  • 能够经过给UIScrollView及其子类添加gestureRecognizer,从而来调用须要处理的事情。

文章如有不对地方,欢迎批评指正

相关文章
相关标签/搜索