响应链

参考文章Understanding Event Handling, Responders, and the Responder Chain。

响应链工作原理

Touch events 是 UIView 常见的点击事件,当手指触摸屏幕上某一控件到响应相关事件分为两部分:

  • 事件的传递
  • 事件的响应

事件的传递涉及了 UIView 里面的两个方法:

// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//判断当前点击事件是否存在最优响应者
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   

// default returns YES if point is in bounds
//判断当前点击坐标是否在点击控件的 bounds 之内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   

事件的传递流程

事件分发和传递.png
  1. 触碰屏幕产生 UIEvent 事件并放入 UIApplication 事件队列中,然后在整个视图结构中自上而下分发。
  2. UIWindow 接收到事件开始逆序遍历 subviews 查找最优响应视图。

如果 hitTest & pointInside 查找到了最优响应视图则后续遍历 subviews 的操作就会终止。

视图查找代码示例:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    //如果视图 alpha < 0.01,userInteractionEnabled = NO,hidden = YES,返回 nil
    if (self.alpha < 0.01 || !self.userInteractionEnabled || self.hidden) return nil;
    //point 是否在当前视图 bounds 之内,如果不在返回 nil
    if (![self pointInside:point withEvent:event]) return nil;
    /*
     typedef NS_OPTIONS(NSUInteger, NSEnumerationOptions) {
     NSEnumerationConcurrent = (1UL << 0),  //当前排序
     NSEnumerationReverse = (1UL << 1),     //倒序
     };
     */
    __block UIView *hitView = nil;
    [self.subviews enumerateObjectsWithOptions:0 usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        hitView = [obj hitTest:point withEvent:event];
        if (hitView) *stop = YES;
    }];
    return hitView ? hitView : self;
}

关于 UIView 逆序遍历 subviews 查找最优响应视图可以用 Method Swizzling 方法交换 hitTest 方法打印查看。

#import "UIView+HitTest.h"
#import <objc/runtime.h>

@implementation UIView (HitTest)

+ (void)load {
    Class class = [self class];
    
    SEL oriSel = @selector(hitTest:withEvent:);
    SEL swiSel = @selector(_hitTest:withEvent:);
    
    Method oriMethod = class_getInstanceMethod(class, oriSel);
    Method swiMethod = class_getInstanceMethod(class, swiSel);
    
    BOOL isAddMethod = class_addMethod(class, oriSel, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (isAddMethod) {
        class_replaceMethod(class, swiSel, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else {
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

- (UIView *)_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"%@ %s",[self class],__PRETTY_FUNCTION__);
    return [self _hitTest:point withEvent:event];
}

这里新建三个 View,View1、View2、View3 并按照顺序加载在 ViewController 上。

依次点击 View1、View2、View3:

//View1
2018-01-02 18:24:53.441840+0800 Response[8173:228607] UIWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.447251+0800 Response[8173:228607] UIView -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.447697+0800 Response[8173:228607] View3 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.447947+0800 Response[8173:228607] View2 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.448151+0800 Response[8173:228607] View1 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.448385+0800 Response[8173:228607] UIStatusBarWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.448513+0800 Response[8173:228607] UIStatusBar_Modern -[UIView(HitTest) _hitTest:withEvent:]
//View2
2018-01-02 18:25:21.644110+0800 Response[8173:228607] UIWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:21.646276+0800 Response[8173:228607] UIView -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:21.646478+0800 Response[8173:228607] View3 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:21.646709+0800 Response[8173:228607] View2 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:21.646848+0800 Response[8173:228607] UIStatusBarWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:21.647004+0800 Response[8173:228607] UIStatusBar_Modern -[UIView(HitTest) _hitTest:withEvent:]
//View3
2018-01-02 18:25:35.386537+0800 Response[8173:228607] UIWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:35.387783+0800 Response[8173:228607] UIView -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:35.388519+0800 Response[8173:228607] View3 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:35.388639+0800 Response[8173:228607] UIStatusBarWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:35.388736+0800 Response[8173:228607] UIStatusBar_Modern -[UIView(HitTest) _hitTest:withEvent:]

事件的响应流程

App 的响应链
  1. 确定最优响应视图,并判断其能否响应事件,如果可以响应则响应链传递终止,反之传递给 nextResponder (通常为 superview)。
  2. 如果事件传递至 UIWindow 且无法响应,则事件传递给 UIApplication,如果 UIApplication 也无法响应,最终事件被抛弃。

并不是所有的 nextResponder 都是其 superview,比如 UIViewController 的根视图 self.view 的 nextResponder 就是其所在 UIViewController。而如果 UIViewController 是 UIWindow 的 rootViewController,那么它的 nextResponder 就是 UIWindow,但如果 UIViewController 是 present 出来的,那么它的 nextResponder 就是之前所执行 present 操作的那个 UIViewController。

总结

Touch event 在 Responder Chain 中的传递与响应:

  • 事件的分发和传递是自上而下的
  • 事件的响应是自下而上的
最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 最近在写一个图片浏览的需求,一些地方我使用了响应者来处理,顺便又去看看了官方文档,这里记录一下官方文档,并给出一些...
    HelloAda阅读 11,057评论 3赞 36
  • 当我们在使用微信等工具,点击扫一扫,就能打开二维码扫描视图。在我们点击屏幕的时候,iphone OS获取到了用户进...
    VincentHK阅读 553评论 0赞 2
  • 首先清楚两个概念响应者:对用户交互动作事件进行响应的对象。响应者链:成为处理事件的响应者的先后顺序链。平时当我们点...
    mengyingguo阅读 450评论 0赞 1
  • 响应链就是一系列相互关联的对象,响应链中的所有对象都是UIResponder的子类。 一般先去定hitTestVi...
    HackerOnce阅读 990评论 1赞 5
  • 漫长的夏日,停不下来的蝉声,大燥,在耳边响了一个暑假,那些星星满天的夜晚,忽略脚边的蚊子,嗡嗡声不止,一轮淡淡的...
    一茶月阅读 301评论 0赞 0

友情链接更多精彩内容