浅谈事件的分发与响应

栏目: IOS · 发布时间: 5年前

内容简介:顾名思义,事件就是发生的一件事,对于APP来说,就是发生的一个操作。具体的就是用户点击一下屏幕就会出现一个我们的主题是探索用户用手指点击屏幕会发生什么,所以我们将注意力放在上面我们了解到,当我们点击了屏幕,就会出现一个事件。既然事件出现了,那么就需要一个一个响应和处理这个事件的对象,那就是我们的

顾名思义,事件就是发生的一件事,对于APP来说,就是发生的一个操作。具体的就是用户点击一下屏幕就会出现一个 事件 (体现为一个 UIEvent ),即一个 触摸事件 。其实,对于 iOS 设备的用户来说,他们操作设备的方式主要有四种方式:触摸屏幕、晃动设备、通过遥控设施控制设备、按压屏幕。 对应的事件类型 UIEventType 有以下三种:

  1. 触屏事件(Touch Event)
  2. 运动事件(Motion Event)
  3. 远端控制事件(Remote-Control Event)
  4. 按压事件(Presses Event)

我们的主题是探索用户用手指点击屏幕会发生什么,所以我们将注意力放在 触摸事件 上。

响应者对象

上面我们了解到,当我们点击了屏幕,就会出现一个事件。既然事件出现了,那么就需要一个一个响应和处理这个事件的对象,那就是我们的 响应者对象 。这些响应者对象都有一个共同的特征,就是他们都继承自 UIResponder 。我们熟知的响应者对象有 UIApplicationUIWindowUIViewController 和所有继承自 UIView 的 UIKit 类

UIResponder

  • 所有响应对象的基类
  • 定义了处理上述各种事件的接口;

第一响应者

在触摸屏幕的事件中:

  • 指的是当前接受触摸的响应者对象(通常是一个UIView对象);
  • 即表示当前该对象正在与用户交互,它是响应者链的开端;
  • 整个响应者链和事件分发的使命都是找出第一响应者。

响应者链条

上面介绍了响应者对象,也知道了 UIApplicationUIWindowUIViewControllerUIView 这些都是响应者。那么一个 APP 会存在很多响应者对象。由这一系列的响应者对象就构成了一个层次结构,那就是 响应者链条

浅谈事件的分发与响应

从上图中可以看到, 响应者链条有以下特点

  1. 响应者链头部通常是由视图( UIView )构成的;
  2. 如果该视图是属于视图控制器( UIViewController )的,那么下一个响应者是该视图控制器,然后再将事件响应到它的父视图( Super View )中;
  3. 如果该视图没有视图控制器( UIViewController ),那么下一个响应者就直接是它的父视图( Super View );
  4. 一直响应直至其对象是单例的窗口( UIWindow
  5. 再下一个响应者就是单例的应用( UIApplication ),也是 响应者链条的终点
  6. 下一个响应者指向 nil ,结束整个循环

事件分发

回到开篇的情况,当用户点击了一下屏幕。系统检测到用户的触摸事件,就会将其打包成一个事件(即 UIEvent 对象),并将这个 UIEvent 对象放入 Application 的事件队列中。这时系统只是知道有这么一个事件发生,虽然 响应者链条 中有很多有处理事件能力的响应者,但是它不知道谁才是响应这个事件的最佳人选。 因此,系统会从 UIApplication 开始,顺着 响应者链条 向上寻找那个最佳的人选。这个寻找的过程就是 事件的分发过程

传递过程

  • 第一步UIApplication 将这个事件从事件队列中拿出来,从顶部开始询问谁才是最佳人选;
  • 第二步UIWindow 会最先获取到事件,并开始使用 hitTest:withEvent: 来判断下面他的子控件中谁才是最佳人选;
  • 第 N - 1 步 :当前 UIView 继续询问他的子视图是不是最佳人选;
  • 第 N 步 :当前 UIView 不是被点击的的视图,orz,上一个 UIView 就是最佳人选了。

从用户视角来看,系统通过 hitTest:withEvent: 方法,从视图的底部一直向表面寻找最佳人选。因为是一直查找,只有在所有的查找都完成了,判断出当前视图没有子视图或者他的子视图都不适合了,那么当前视图就是最佳人选了。(所以你只是点了一个你一眼就看中的视图,其实系统是从底部开始,一顿连续操作才找到你想要的东西[汗颜])

hitTest:withEvent:

上面的事件分发过程中,大量使用了 hitTest:withEvent: 这个方法,它的处理流程如下:

  • 首先调用当前视图的 pointInside:withEvent: 方法, 判断触摸点是否在当前视图内
    • 若返回 NO ,则 hitTest:withEvent: 返回 nil
    • 若返回 YES ,则向当前视图的所有子视图发送 hitTest:withEvent: 消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从 subviews 数组的末尾向前遍历。
  • 若有子视图返回非空对象,则 hitTest:withEvent: 方法返回此对象,处理结束;
  • 若所有子视图都返回 nil ,则 hitTest:withEvent: 方法返回自身,即 self ,处理结束。

下面我们用一个图解来理解一下这个 hitTest:withEvent:

浅谈事件的分发与响应

假如用户点击了 View D ,结合上图详细介绍一下 hitTest:withEvent: 过程: ( hitTest:withEvent: 简称 hitTestpointInside:withEvent: 简称 pointInsideView X 简称 X

  1. A 是 UIWindow 的 根视图 ,因此,UIWindow 对象会首先对 A 进行 hitTest
  2. 显然用户点击的范围是在 A 的范围内,这时会继续检查 A 的子视图;
  3. 这时候会有 B 和 C 两个分支,由于 C 是后添加的子视图,因此先对 C 进行 hitTest
    • 显然点击的范围在 C 内;
  4. 这时候有 D 和 E 两个分支,按顺序先检查 E
    hitTest:withEvent:
    hitTest
    
  5. 因此,D 的 hitTest 会将 D 返回,再往回回溯,就是 C 的 hitTest 返回 D,A 的 hitTest 返回 D。

至此,本次点击事件的 第一响应者 就通过响应者链的事件分发逻辑成功找到了

除了使用 pointInside:withEvent: 判断是否是响应者,还有下面三种情况会使 hitTest:withEvent: 返回 nil:

hidden=YES
userInteractionEnabled=YES
alpha<0.01

因此 hitTest:withEvent: 的实现可能是:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    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];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}
复制代码

事件响应

前面说了一大堆事件的分发,其实就是为了找到响应事件的最佳人选,这个最佳人选就是在介绍 响应者链条 的时候,最底下的那个 View 。从这个 View 开始我们沿着 响应者链条 的方向进行响应。

开篇我们的说的是用户点击屏幕的场景,因此,响应者会按照当前 UITouch 的所处阶段使用下面的方法进行响应:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
复制代码
  • 在响应方法内部,我们也可以调用调用 [super touches...] 将这个触摸事件继续分发给父控件的对应方法处理。然后父控件还可以将该事件继续向上传递,直到传递给UIApplication对象。这一系列的响应者对象就 构成了一个响应者链条
  • 如果不调用 [super toucher...] 事件 不会继续沿着响应者链条进行响应

小结

事件的 分发响应 都是在 响应者链条 上进行的,只不过是两者 传递的方向 不同。

浅谈事件的分发与响应
上面的图片中省略了 UIViewController

,这里说明一下他的位置:

  • 事件分发 过程中没有 ViewController 的事
  • 事件响应 的过程中,传递的方向如下:
浅谈事件的分发与响应

至此,我们已经大概了解了当用户用手指点击了一下屏幕,会发生什么。

通过对这些的了解,我们可以通过使用下面两种方式来实现一些特殊需求:

hitTest:withEvent:
touches

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

High Performance JavaScript

High Performance JavaScript

Nicholas C. Zakas / O'Reilly Media / 2010-4-2 / USD 34.99

If you're like most developers, you rely heavily on JavaScript to build interactive and quick-responding web applications. The problem is that all of those lines of JavaScript code can slow down your ......一起来看看 《High Performance JavaScript》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具