iOS Touch事件简析

Touch事件

iOS的Touch Event一般指的是对屏幕的触摸操作。说到触摸响应,就不能不提Responder了。而且Responder自有一套体系,称之为Responder Chain:响应者链。

响应者链(Responder Chain)

响应者链是由若干个响应者对象构成的层次结构。响应者对象(Responder Object),指的是能够相遇和处理触摸事件的对象。

那么,响应者对象是什么?能够处理响应事件的就是响应者。UIResponder是Cocoa中处理响应的类。而UIApplication、UIView、UIViewController这几个类都是直接继承自UIResponder类,这几个类的实例都可称之为响应者对象。

通常一个iOS界面视图展示在用户面前即形成了响应者链结构。用户在进行触摸操作的时候,一般触摸到的是最顶层的UIView,即第一响应者,如果第一响应者不能处理该事件,则事件继续往下层传递,直到UIApplication结束。如下图所示流程:

响应者链流程示意

  • 第一响应者(First Responder

响应者链怎么确定哪个是第一个响应者?当触摸事件发生后,UIKit会创建一个事件对象(UIEvent),该对象包含一些处理事件所需要的信息。并放入当前活动Application的事件队列。处理事件时,程序的UIApplication对象会从队列头部取出一个事件对象,通常将其分发给程序的主window对象。window对象会首先尝试将事件分发给触摸事件发生的那个视图上。这一视图通常被称为hit-test视图,而查找这一视图的过程就叫做hit-testing

hit-testing过程涉及到两个重要函数:pointInside:withEvent:和hitTest:withEvent:。

  1. 触摸事件发生时,UIWindow实例对象会尝试调用hitTest:withEvent:函数来获取hit-test视图。
  2. hitTest:withEvent:函数会调用pointInside:withEvent:函数。pointInside:withEvent:函数根据insidePoint来判断是否在目标视图内,如果不在则返回NO。hitTest:withEvent:函数返回nil。如果在范围内则返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕。
  3. 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此视图对象,处理结束。
  4. 如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。
    下面是Apple Dev的一张图:

假设用户点击了视图E,系统按照以下顺序来查找hit-test视图:

  1. 点击事件发生在视图A的边界内,所以检测子视图B和C。
  2. 点击事件不在视图B的边界内,但在视图C的边界范围内,所以检测子图片D和E。
  3. 点击事件不在视图D的边界内,但在视图E的边界范围内。
    视图E是包含触摸点的视图层次架构中最底层的视图(倒树结构),所以它就是hit-test视图。

hit-test视图可以最先去处理触摸事件,如果hit-test视图不能处理事件,则事件会沿着响应链往上传递,直到找到能处理它的视图。

这里需要注意的几点是:

  1. 查找过程中可能会有几个hit-test视图包含了insidePoint,但这些hit-test视图并不绝对是第一响应者,只有最后一个hit-test视图才是第一响应者。
  2. 如果最终hit-test没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯,如果UIWindow实例和UIApplication实例都不能处理该事件,则该事件会被丢弃。
  3. hitTest:withEvent:方法将会忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。
  4. 如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds 属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:方法来处理这种情况。

事件传递

最有机会处理事件的对象是hit-test视图或第一响应者。如果这两者都不能处理事件,UIKit就会将事件传递到响应链中的下一个响应者。每一个响应者确定其是否要处理事件或者是通过nextResponder方法将其传递给下一个响应者。这一过程一直持续到找到能处理事件的响应者对象或者最终没有找到响应者。

如图流程:

当系统检测到一个事件时,将其传递给初始对象,这个对象通常是一个视图。然后,会按以下路径来处理事件(我们以左图为例):

  1. 初始视图(initial view)尝试处理事件。如果它不能处理事件,则将事件传递给其父视图。
  2. 初始视图的父视图(superview)尝试处理事件。如果这个父视图还不能处理事件,则继续将视图传递给上层视图。
  3. 上层视图(topmost view)会尝试处理事件。如果这个上层视图还是不能处理事件,则将事件传递给视图所在的视图控制器。
  4. 视图控制器会尝试处理事件。如果这个视图控制器不能处理事件,则将事件传递给窗口(window)对象。
  5. 窗口(window)对象尝试处理事件。如果不能处理,则将事件传递给单例app对象。
  6. 如果app对象不能处理事件,则丢弃这个事件。
    从上面可以看到,视图、视图控制器、窗口对象和app对象都能处理事件。另外需要注意的是,手势也会影响到事件的传递。