事件响应和响应者链

iOS 的事件主要分为以下几类:

Touch Events(触摸事件)

Motion Events(运动事件,比如重力感应、摇一摇等)

Remote Events(远程事件 ,比如用耳机上的事件来控制手机)

事件传递中UIWindow会根据不同的event,用不同的方式寻找initial object,initial object决定于当前的事件类型。比如Touch Event,UIWindow会首先试着把事件传递给事件发生的那个view,就是下文要说的hit-testview。对于Motion和Remote Event,UIWindow会把例如震动或者远程控制的事件传递给当前的firstResponder。

注:以下只讨论触摸事件

响应者链(Responder chain)

响应者对象是指能够对用户交互事件进行响应的对象,响应者链是由一系列响应者对象构成顺序链。整个app就通过nextResponder串成了一条链,也就是我们所说的响应链。所以响应链就是一条虚拟的链,并没有一个对象来专门存储这样的一条链,而是通过UIResponder的属性串连起来的。

事件的产生及传递

事件的产生

发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈,是因为队列是先进先出,而栈是先进后出,先产生的事件应该先处理。

UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常事件先发送给应用程序的主窗口(keyWindow)。

主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

事件的传递

触摸事件的传递是从父视图传递到子视图的,即UIApplication->Window->寻找处理事件最合适的view

如果父视图不能接收触摸事件,那么子视图就不可能接收到触摸事件。

如何找到最合适的视图(Hit-Testing View)来处理事件

1.首先判断视图是否可以接受触摸事件

2.判断触摸点是否在视图上

3.子视图数组中从后往前遍历,重复前面两个操作

4.如果没有符合条件的子视图,那么就认为自己是最合适的事件处理者,也就是最合适的视图

UIView不能接收触摸事件的三种情况

不允许交互:userInterationEnable = NO

隐藏:如果把父视图隐藏,那么子视图也会隐藏,隐藏的视图不能接收触摸事件

透明度:如果设置一个视图的透明度<=0.01,则该视图不能接收触摸事件

如何判断触摸点是否在视图上

Hit-Test 机制

当用户触摸(Touch)屏幕进行交互时,系统首先要找到响应者(Responder)。系统检测到手指触摸(Touch)操作时,将Touch 以UIEvent的方式加入UIApplication事件队列中。UIApplication从事件队列中取出最新的触摸事件进行分发传递到UIWindow进行处理。UIWindow 会通过hitTest:withEvent:方法寻找触碰点所在的视图,这个过程称之为hit-test view。

那么什么是Hit-Test呢,我们可以把它理解为一个探测器,通过这个探测器我们可以找到并判断手指是否点击在某个视图上面,换句话说就是通过Hit-Test可以找到手指点击到的处于屏幕最前面的那个UIView。

在解释Hit-Test是怎么工作之前,先来看看它是什么时候被调用的。前面说Hit-Test是一个探测器,那么在代码里面其实就是一个函数,UIView有如下两个方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

每当手指接触屏幕,UIApplication接收到手指的事件之后,就会去调用UIWindow的hitTest:withEvent:,看看当前点击的点是不是在window内,如果是则继续依次调用subView的hitTest:withEvent:方法,直到找到最后需要的view。调用结束并且hit-test view确定之后,这个view和view上面依附的手势,都会和一个UITouch的对象关联起来,这个UITouch会作为事件传递的参数之一,我们可以看到UITouch头文件里面有一个view和gestureRecognizers的属性,就是hitTest view和它的手势。

hitTest 的顺序如下

UIApplication -> UIWindow -> Root View -> ··· -> subview

在视图上调用pointInside:withEvent:方法判断触摸点是否在当前视图内;

如果返回NO,那么hitTest:withEvent:返回nil;

如果返回YES,那么它会从后往前遍历子视图数组执行上述操作,直到有子视图返回非空对象或者全部子视图遍历完毕。

如果有subview的hitTest:withEvent:返回非空对象,则处理结束(注意这个过程,子视图也是根据pointInside:withEvent:的返回值来确定是返回空还是当前子视图对象的。并且这个过程中如果子视图的hidden=YES、userInteractionEnabled=NO或者alpha小于等于0.01都会并忽略);

如果所有subview遍历结束仍然没有返回非空对象,则hitTest:withEvent:返回self;

系统就是这样通过hit test找到触碰到的视图(Initial View)进行响应。

有了事件响应链,接下来的事情就是寻找响应事件的具体响应者了,我们称着为:Hit-Testing View,寻找这个View的过程我们称着为Hit-Test。

响应者链顺序如下:

Initial View -> View Controller(如果存在) -> superview -> · ··  -> rootView -> UIWindow -> UIApplication

如果一个View有一个视图控制器(View Controller),它的下一个响应者是这个视图控制器,紧接着才是它的父视图(Super View),如果一直到Root View都没有处理这个事件,事件会传递到UIWindow(iOS中有一个单例Window),此时Window如果也没有处理事件,便进入UIApplication,UIApplication是一个响应者链的终点,它的下一个响应者指向nil,以结束整个循环

事件传递的完整过程

先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件。 调用最合适控件的touches….方法 如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者 接着就会调用上一个响应者的touches….方法。(如果视图不能处理接收到的事件或者消息,就会沿着响应者链向上传递直到能够响应该事件的视图,如果都不能响应则舍弃)

推荐阅读更多精彩内容