iOS 触摸事件的传递与响应

status
category
date
summary
slug
icon
tags
password
绘制过程和事件分发都是 Android 和 iOS 中经常谈到的话题,虽然实习已经结束了,但依然记得当时刚开始学习 iOS,有人问我UIButton继承自哪个类,我想了想说:“这个我不太清楚,但 Android 中 Button 是继承自 TextView,所以我觉得 UIButton 应该继承自 UILabel ……”现在我终于明白了为什么对方当时一脸懵逼,也很庆幸他没有继续问事件分发机制。所以现在我怀着感恩的心,学习一下相关知识。

从触摸到事件产生

iOS 中有四种事件,包括触摸事件、运动事件、远程控制事件、按压事件,本文主要讨论最常见的触摸事件。事件描述了用户和应用的交互行为,而 UIEvent 就是记录事件的对象,它记录了事件发生的时间、类型,下面是 UIEvent 的几个属性(还有可以获取 UIWindowUIView 对应的 UITouch 集合等其他属性没有列出):
通过 allTouches 属性可以知道,一个 UIEvent 对象可以包含多个 UITouch 对象,即一个触摸事件可能有多次触摸,例如三指触摸等等。UITouch 详细记录了手指在屏幕上产生的交互,例如时间、位置、所在的窗口和视图等等(还有力度、类型 UITouchType 等属性没有列出):
当事件产生之后,系统会寻找最合适的响应者对象来处理事件,如果找不到任何可以响应事件的对象,事件就会被废弃。只有继承自 UIResponder 的对象才能响应事件,常见的它的子类有 UIApplicationUIViewUIViewController。这是它们的继承关系:
notion image
当手指触摸屏幕,系统会创建事件对象,并放入 UIApplication 管理的事件队列,队列的特点是 FIFO,即先进先出,符合先产生的事件先被处理的逻辑,到这里触摸事件就产生了。

事件的传递

UIApplication 会不断地从事件队列中取出事件,并传递下去,找到最合适的响应者来处理事件。在 iOS UIView绘制(二)View Hierarchy 一文中说到,UIApplication 是通过 UIWindow 来管理所有视图的,具体关系如下图所示。所以一般来说,事件会首先传递给程序的主窗口(keyWindow),然后由主窗口向下传递。
notion image
因为事件是从父 view 向子 view 传递的,所以如果父 view 不能接收触摸事件,那么子 view 也不能接收到这个事件。事件的传递是一个递归的过程,传递这个动作实际上指的是 hitTest 这个方法:
顾名思义,hitTest方法可以返回最适合的响应者,point 方法会判断触摸点是否处于视图之中,寻找响应者的过程可以用以下代码来表示:
需要注意的有这么几点:
  • 能够接收事件需要同时满足三个条件:
    • 用户可以交互(isUserInteractionEnabled = true
    • 不被隐藏(isHidden = false
    • 透明度不低于0.01(alpha >= 0.01
  • 遍历是从 subviews 的最后一个元素开始的,因为最后一个元素“Z轴最高”,发生重叠时会覆盖之前的视图,符合触摸时上层视图优先响应的原则
  • 如果触摸的点不在父 view 上,那么子 view 的 hitTest 就不会调用。即使子 view 的大小超过了父 view,如果触摸发生在子 view 超出父 view 的区域内,依然不会返回子 view
到这里一个响应者返回时,事件就结束了传递,接下来就是事件的响应了。

事件的响应

事件从 UIApplication 的事件队列传递到到 UIWindow 再到具体的 UIView,过程中经历了一系列对象,除了 UIApplication,大致可以看做是父 view 把事件传递到子 view,最后找到了响应者,是一个自上向下的过程。但事件的响应可以看做是传递的逆过程,事件从响应者逐步传递到父 view,再一次经历之前的一系列对象依次处理,这些对象称为响应者链
在响应者链中,响应者要么处理事件,要么把事件“传递”给下一个响应者,注意这里的传递是指响应过程中的传递,不要和上面事件的传递过程混淆。
notion image
在上面这个例子中,如果我们点击输入框,并且 UITextField 没有处理事件,那么事件会被传递到响应者链中的下一个响应者,即图中的 UIView,紧接着就是 UIViewControllerRoot View,然后是 UIViewController,再是 UIWindow。如果 UIWindow 也没有处理事件,事件就会被移交给 UIApplication,再由 UIApplicationDelegate 处理,前提是它继承自 UIResponder
上面说的事件处理,实际上指的是重写 touches 相关的方法:
另外,上面的例子中,如果第一个响应者 UITextField 重写了这些方法,也就是处理了事件,可以选择是否重写 touches 方法,来控制事件是否传递到让下一个响应者,而下一个响应者一般是父 view:
如果调用了 nextResponder 的 touches 对应方法,就达到了一个事件被多个对象处理的目的。

总结

事件从产生到处理主要经历了两个过程,第一个过程是事件的传递,这是一个从 UIApplicationUIWindow 及其子 view 的递归过程,同时是一个自顶向下的过程,这个过程的最后一个对象就是最适合的响应者,这个过程的结束也标志着传递过程的结束;第二个过程是事件的响应,同时这也是事件在响应者链上的传递,不过是从子 view 到 UIWindow,再到 UIApplication 或者 UIApplicationDelegate 的一个自下向上的过程。
有一点值注意的是,事件的传递实际上并不注重过程,而是注重结果,传递过程的目的就是找到最适合的响应者,UIApplicationUIWindow 都只是寻找到最后那个 UIView 的工具,使用这些工具的技巧就是四大组件之间的关系,具体的入口就是 keyWindowsubviews。但事件的响应是注重过程的,每个响应者是否处理和传递事件都是我们需要关心的,在这个过程中,UIView 的属性显得不那么重要,我们只关心 UIResponder。这也解释了 UIViewController 在两个过程中角色的差异,传递时因为不是 UIView 而划水,响应时因为是 UIResponder 而成为响应者链的一部分。
Loading...

© 刘口子 2018-2025