OC 响应链

2018-12-02  本文已影响0人  苏沫离

当用户点击屏幕时:屏幕接收点击信号将点击位置转换成具体坐标,然后本次点击被包装成一个点击事件 UIEvent;最终会在某个视图响应本次事件进行处理,而为 UIEvent 查找响应视图的过程被称为响应链查找。

一个点击事件是如何从屏幕传递到处理视图?响应链又是什么呢?

1、响应者 UIResponder

响应者是响应链中的一个节点,是可以处理事件的具体对象,一个响应者应当是UIResponder 或其子类的实例对象。从API设计上来看,UIResponder 主要提供了三类接口:

1.1、由nextResponder 指针构成的链表

每个UIResponder 都有nextResponder ,这些nextResponder指针指向下一个UIResponder,由此构成树状结构。打印 Demo 界面上所有控件的nextResponder

当前responder AppDelegate ============ nextResponder (null)
当前responder UIApplication ============ nextResponder AppDelegate
当前responder UIWindow : 10 ============ nextResponder UIApplication
当前responder UIView : 20 ============ nextResponder ViewController
当前responder UILabel : 11 ============ nextResponder UIWindow : 10
当前responder UIButton : 12 ============ nextResponder UIWindow : 10
当前responder ViewController ============ nextResponder UIWindow : 10
当前responder UIButton : 21 ============ nextResponder UIView : 20
当前responder UIButton : 22 ============ nextResponder UIView : 20
当前responder UILabel : 23 ============ nextResponder UIView : 20

分析打印结果:

这些UIRespondernextResponder 指针构成一个树状结构,如下如所示:

Demo中所有UIResponder的nextResponder关系图.png
1.2、响应链是否由 UIResponder构成?

nextResponder 组成的链表是不是响应链呢?
假如nextResponder 组成响应链,那么点击 lablebutton 的重叠部分,应该是 lable.superView 响应事件。但实际却是 button 响应点击事件。
所以由 nextResponder 组成的链表并不是响应链的全部!

1.3、nextResponder 使用

利用 nextResponder ,可以获取 UIView 所在的 UIViewController

- (UIViewController *)viewController{
    UIResponder *nextVC = self.nextResponder;
    while ([nextVC isKindOfClass:UIViewController.class] == NO)
        nextVC = nextVC.nextResponder;
    return [nextVC isKindOfClass:UIViewController.class] ? (UIViewController *)nextVC : nil;
}

2、视图 UIView

UIResponder继承关系.png

虽然 UIResponder 提供了处理响应事件的能力,但它无法被用户观察到;换句话说,用户无法点击这些有处理能力的对象。
UIView 继承 UIResponder,是展示在界面上的可视元素,包括不限于文本、按钮、图片等可见样式。既展示了界面元素,又具有与用户交互的能力:

实际上,查找响应者就是查找点击坐标落点位置在其可视范围内且其具备处理事件能力的对象;也就是既要Responder 又要View 的对象。UIView 提供了两个重要的方法用于查找响应视图:

/* 获取响应视图
 * 该方法内部可以分为三大步骤:
 * @step1 判断视图是否可交互 userInteractionEnabled=NO,alpha < 0.01,hidden=YES
 * @step2 判断视图是否在点击范围,调用 -pointInside:withEvent:
 * @step3 倒序遍历所有子视图,子视图调用 -hitTest:withEvent:
 *
 * @note 上述任一步骤不通过,直接返回 nil ,不必向下执行
 * @note 倒序查询优化查找速度,毕竟后添加的视图在上方易于被用户点击
 */
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  

// 检测触摸点是否落在当前视图范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   
2.1、-hitTest:withEvent:方法

通过方法交换exchang,打印一些关键信息:

- (BOOL)l_pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    NSLog(@"当前视图 pointInside ======== %@ : %ld",self.class,self.tag);
    BOOL pointInside = [self l_pointInside:point withEvent:event];
    if (pointInside) {
        NSLog(@"当前视图范围内 ====== %@ : %ld ",self.class,self.tag);
    }else{
        NSLog(@"当前视图范围外 ====== %@ : %ld ",self.class,self.tag);
    }
    return pointInside;
}

- (UIView *)l_hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    NSLog(@"当前视图     hitTest -------- %@ : %ld",self.class,self.tag);
    UIView *responserView = [self l_hitTest:point withEvent:event];
    if (responserView) {
        NSLog(@"当前视图 %@ : %ld -- 响应视图 %@ : %ld ",self.class,self.tag,responserView.class,(long)responserView.tag);
    }else{
         NSLog(@"当前视图 %@ : %ld -- 响应视图 查找不到",self.class,self.tag);
    }
    return responserView;
}

运行Demo,点击 ViewController.view 空白区域,观察上述方法的打印日志:

控制台打印数据分析.jpg

通过打印数据,分析 -hitTest:withEvent: 的内部实现:

2.2、尝试重写 -hitTest:withEvent:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    ///第一步:判断视图是否可交互
   if (self.userInteractionEnabled == NO || self.alpha < 0.01 || self.hidden == YES) {
       return nil;
   }
   //第二步:判断视图是否在点击范围内
   if ([self pointInside:point withEvent:event] == NO) {
       return nil;
   }
   //第三步:倒序遍历所有子视图
   __block UIView *resultView = self;
   [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
       UIView *view = [obj hitTest:point withEvent:event];
       if (view) {
           resultView = view;
           *stop = YES;
       }
   }];
   return resultView;
}
2.3、-hitTest:withEvent: 方法使用

此方法可实现点击穿透、点击下层视图等功能:
eg1:一般来说,子视图在父视图之外区域的触摸操作不会被识别;重写父视图的 -pointInside:withEvent: 识别该视图返回 YES,则可以相应该操作;
eg2:扩大button 的点击区域;

3、事件处理

通过对-hitTest:withEvent:方法的分析,我们知道事件的传递过程。那么最终 UIView 能够处理该次点击事件嘛?

//UIResponder 提供了四个方法用于处理事件:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

如果需要最终视图处理事件,重写ViewController的上述方法:

- (void)touchesBegan: (NSSet<UITouch *> *)touches withEvent: (UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    NSLog(@"began --- %ld",(long)self.tag);
}

- (void)touchesEnded: (NSSet<UITouch *> *)touches withEvent: (UIEvent *)event {
    NSLog(@"ended ---- %ld",(long)self.tag);
}

/* 点击空白区域的打印数据:
began --- 10
began --- 20
ended ---- 20
ended ---- 10
 */

ViewControllerUIWindow 都处理事件了;注释//[super touchesBegan:touches withEvent:event]; 再次执行程序:

began --- 20
ended ---- 20

这次只有 ViewController处理事件。

-hitTest:withEvent: 返回的 UIView 对象是最可能处理事件的对象,但该对象并不一定处理事件。这时,需要逆着查找链找到能处理该事件的对象。

3.1 、响应链

响应链可以分为事件的传递链、事件的响应链:

事件处理完毕后,主线程的 Runloop 开始休眠,等待下一个事件!

引用iOS触摸事件的流动的一张流程图:

整个iOS触摸事件从产生到寂灭.png

Demo

参考文章

hitTest:withEvent:方法流程
分析实现-聊聊响应链

上一篇 下一篇

猜你喜欢

热点阅读