美文网首页
(三)寻找事件的最佳响应者(Hit-Testing)

(三)寻找事件的最佳响应者(Hit-Testing)

作者: 小白猿 | 来源:发表于2019-03-01 12:52 被阅读0次

本文系转载,原文地址为iOS触摸事件全家桶

APP接收到触摸事件后,会被放入当前应用的一个事件队列中,然后寻找
一个具备最高优先级响应权的响应对象(the most appropriate responder object),这个过程叫做Hit-Testing,那个命中的最佳响应者称为hit-tested view,找到这个view来响应它,然后释放

本节要探讨的问题是:

  1. 应用接收到事件后,如何寻找最佳响应者?底层如何实现?
  2. 寻找最佳响应者过程中事件的拦截。

事件自下而上的传递

  1. 应用接收到事件后先将其置入事件队列中以等待处理。
  2. 出队后,application首先将事件传递给当前应用最后显示的窗口(UIWindow)询问其能否响应事件。
    若窗口能响应事件,则传递给子视图询问是否能响应,子视图若能响应则继续询问子视图。
    子视图询问的顺序是优先询问后添加的子视图,即子视图数组中靠后的视图。事件传递顺序如下:
    UIApplication ——> UIWindow ——> 子视图 ——> ... ——> 子视图
  3. 事实上把UIWindow也看成是视图即可,这样整个传递过程就是一个递归询问子视图能否响应事件过程,且后添加的子视图优先级高(对于window而言就是后显示的window优先级高)。
    具体流程如下:
    (1)UIApplication首先将事件传递给窗口对象(UIWindow),若存在多个窗口,则优先询问后显示的窗口。
    (2) 若窗口不能响应事件,则将事件传递其他窗口;若窗口能响应事件,则从后往前询问窗口的子视图。
    (3) 重复步骤2。即视图若不能响应,则将事件传递给上一个同级子视图;若能响应,则从后往前询问当前视图的子视图。
    (4) 视图若没有能响应的子视图了,则自身就是最合适的响应者。

示例:



视图层级如下(同一层级的视图越在下面,表示越后添加):

A
├── B
│   └── D
└── C
    ├── E
    └── F
现在假设在E视图所处的屏幕位置触发一个触摸,应用接收到这个触摸事件事件后,先将事件传递给UIWindow,然后自下而上开始在子视图中寻找最佳响应者。事件传递的顺序如下所示:

(0) UIWindow将事件传递给其子视图A
(1) A判断自身能响应该事件,继续将事件传递给C(因为视图C比视图B后添加,因此优先传给C)。
(2) C判断自身能响应事件,继续将事件传递给F(同理F比E后添加)。
(3) F判断自身不能响应事件,C又将事件传递给E。
(4) E判断自身能响应事件,同时E已经没有子视图,因此最终E就是最佳响应者。


Hit-Testing的本质

视图如何判断能否响应事件?以及视图如何将事件传递给子视图?
首先要知道的是,以下几种状态的视图无法响应事件:

  • 不允许交互:userInteractionEnabled = NO
  • 隐藏:hidden = YES 如果父视图隐藏,那么子视图也会隐藏,隐藏的视图无法接收事件
  • 透明度:alpha < 0.01 如果设置一个视图的透明度<0.01,会直接影响子视图的透明度。alpha:0.0~0.01为透明。

每个UIView对象都有一个 hitTest:withEvent: 方法,这个方法是Hit-Testing过程中最核心的存在,其作用是询问事件在当前视图中的响应者,同时又是作为事件传递的桥梁
hitTest:withEvent:方法返回一个UIView对象,作为当前视图层次中的响应者。默认实现是:

  • 若当前视图无法响应事件,则返回nil
  • 若当前视图可以响应事件,但无子视图可以响应事件,则返回自身作为当前视图层次中的事件响应者
  • 若当前视图可以响应事件,同时有子视图可以响应,则返回子视图层次中的事件响应者
  1. 一开始UIApplication将事件通过调用UIWindow对象的hitTest:withEvent: 传递给UIWindow对象
  2. UIWindowhitTest:withEvent: 在执行时若判断本身响应事件,则调用子视图的 hitTest:withEvent: 将事件传递给子视图,并询问子视图上的最佳响应者。最终UIWindow返回一个视图层次中的响应者视图给UIApplication,这个视图就是hit-testing的最佳响应者。

系统对于视图能否响应事件的判断逻辑除了之前提到的3种限制状态(交互,隐藏,透明度),默认能响应的条件就是触摸点在当前视图的坐标系范围内(即方法pointInside:withEvent:)。因此,hitTest:withEvent: 的默认实现就可以推测了,大致如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    //3种状态无法响应事件
     if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil; 
    //触摸点若不在当前视图上则无法响应事件
    if ([self pointInside:point withEvent:event] == NO) return nil; 
    //从后往前遍历子视图数组 
    int count = (int)self.subviews.count; 
    for (int i = count - 1; i >= 0; i--) 
    { 
        // 获取子视图
        UIView *childView = self.subviews[i]; 
        // 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
        CGPoint childP = [self convertPoint:point toView:childView]; 
        //询问子视图层级中的最佳响应视图
        UIView *fitView = [childView hitTest:childP withEvent:event]; 
        if (fitView) 
        {
            //如果子视图中有更合适的就返回
            return fitView; 
        }
    } 
    //没有在子视图中找到更合适的响应视图,那么自身就是最合适的
    return self;
}

值得注意的是pointInside:withEvent: 这个方法,用于判断触摸点是否在自身坐标范围内。默认实现是若在坐标范围内则返回YES,否则返回NO。
现在我们在上述示例的视图层次中的每个视图类中添加下面3个方法来验证一下之前的分析(注意 hitTest:withEvent:pointInside:withEvent: 方法都要调用父类的实现,否则不会按照默认的逻辑来执行Hit-Testing):

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    return [super hitTest:point withEvent:event];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    return [super pointInside:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
}

单点触摸视图E,相关日志打印如下:

-[AView hitTest:withEvent:]
-[AView pointInside:withEvent:]
-[CView hitTest:withEvent:]
-[CView pointInside:withEvent:]
-[FView hitTest:withEvent:]
-[FView pointInside:withEvent:]
-[EView hitTest:withEvent:]
-[EView pointInside:withEvent:]
-[EView touchesBegan:withEvent:]

可以看到最终是视图E先对事件进行了响应,同时事件传递过程也和之前的分析一致。
事实上单击后从 [AView hitTest:withEvent:][EView pointInside:withEvent:]的过程会执行两遍,两次传的是同一个touch,区别在于touch的状态不同,第一次是begin阶段,第二次是end阶段。也就是说,应用对于事件的传递起源于触摸状态的变化。


Hit-Testing过程中的事件拦截(自定义事件流向)

实际开发中可能会遇到一些特殊的交互需求,需要定制视图对于事件的响应。例如下面Tabbar的这种情况,中间的原型按钮是底部Tabbar上的控件,而Tabbar是添加在控制器根视图中的。默认情况下我们点击图中红色方框中按钮的区域,会发现按钮并不会得到响应。

分析一下原因其实很容易就能明白问题所在。忽略不相关的控件,视图层次如下:

RootView
└── TableView
└── TabBar
    └── CircleButton

点击红色方框区域后,生成的触摸事件首先传到UIWindow,然后传到控制器的根视图即RootViewRootView经判断可以响应触摸事件,而后将事件传给了子控件TabBar
问题就出在这里,因为触摸点不在TabBar的坐标范围内,因此TabBar无法响应该触摸事件,hitTest:withEvent: 直接返回了nil。而后RootView就会询问TableView是否能够响应,事实上是可以的,因此事件最终被TableView消耗。整个过程,事件根本没有传递到圆形按钮
有问题就会有解决策略。经过分析,发现原因是hit-Testing的过程中,事件在传递到TabBar的时候没能继续往CircleButton传,因为点击区域坐标不在Tabbar的坐标范围内,因此Tabbar被识别成了无法响应事件。既然如此,我们可以修改事件hit-Testing的过程,当点击红色方框区域时让事件流向原型按钮。
事件传递到TabBar时,TabBarhitTest:withEvent: 被调用,但是 pointInside:withEvent: 会返回NO,如此一来 hitTest:withEvent: 返回了nil。既然如此,可以重写TabBard的pointInside:withEvent:,判断当前触摸坐标是否在子视图CircleButton的坐标范围内,若在,则返回YES,反之返回NO。这样一来点击红色区域,事件最终会传递到CircleButton,CircleButton能够响应事件,最终事件就由CircleButton响应了。同时点击红色方框以外的非TabBar区域的情况下,因为TabBar无法响应事件,会按照预期由TableView响应。代码如下:

//TabBar
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    //将触摸点坐标转换到在CircleButton上的坐标
    CGPoint pointTemp = [self convertPoint:point toView:_CircleButton];
    //若触摸点在CricleButton上则返回YES
    if ([_CircleButton pointInside:pointTemp withEvent:event]) {
        return YES;
    }
    //否则返回默认的操作
    return [super pointInside:point withEvent:event];
}

这样一来,点击红色方框区域的按钮就有效了。

下一篇 (四)事件的响应及在响应链中的传递

相关文章

  • (三)寻找事件的最佳响应者(Hit-Testing)

    本文系转载,原文地址为iOS触摸事件全家桶 APP接收到触摸事件后,会被放入当前应用的一个事件队列中,然后寻找一个...

  • iOS 手势小记(一)

    关于手势的几个属性的记录 关于iOS的事件响应者链的关系hit-testting 寻找最佳响应者大家应该不陌生.但...

  • 一文搞懂响应者链

    以往每次在处理到拦截事件或者传递事件的时候,会想到响应者链,通过重写hit-testing方法去修改,但是真正写的...

  • 响应链是如何工作

    那么响应链是如何工作,正确找到应该响应该事件的响应者的?UIKit使用基于视图的hit-testing来确定tou...

  • 响应者链

    一. Hit-Testing 什么是Hit-Testing?对于触摸事件, window首先会尝试将事件交给事件触...

  • 事件的分发和传递

    响应者: 在iOS中,响应者为能响应事件的UIResponder子类对象,如UIButton、UIView等。 响...

  • iOS 使用响应者和响应者链处理事件

    内容概览 综述 确定一个事件的第一响应者(First Responder) 确定哪个响应者包含一个触控事件 改变响...

  • dailyLearning -- 响应者链

    响应者对象介绍 什么是响应者链 事件响应流程(事件的产生和传递) 怎么寻找最合适的 view 应用 在runLoo...

  • iOS 响应链

    iOS开发 - 事件传递响应链iOS 响应者链,事件的传递事件传递之响应链Cocoa Touch事件处理流程--响...

  • 响应者链

    什么是响应者链 在iOS的事件传递过程中需要通过多个响应者来寻找最合适的事件接收者,把这个过程中的响应者串联起来就...

网友评论

      本文标题:(三)寻找事件的最佳响应者(Hit-Testing)

      本文链接:https://www.haomeiwen.com/subject/xvcduqtx.html