源码

iOS的事件传递: 响应者链条

一. iPhone操作系统下的事件

事件是被发送到应用程序中反应用户操作的对象。当用户触发一个事件时,UIKit创建一个包含处理事件信息的事件对象。然后把它放在活跃app的时间队列中。像触摸事件,是被打包在UIEvent对象中的一组touches。像运动事件,事件对象的不同取决于你使用哪个框架和对什么类型的运动事件感兴趣。   

事件沿着特定的路径传递直到被传递给一个能处理它的对象。 最初,UIApplication单例对象从队列头部接收事件并派发下去处理。通常,事件被传递给app的主窗口,它将事件传递给初始对象处理,这取决于事件的类型。

触摸事件

对于触摸事件而言, 窗口对象首先试着将其传递给触摸发生的视图,这个视图被称为 hit-test view。查找 hit-test view的过程被称为hit-testing。

运动和远程控制事件

对于这类事件,窗口对象将摇晃运动或者远程控制事件传递给第一响应者来处理。

事件传递的最终目的是找出一个能处理并响应事件的对象。UIKit 会将事件传递给最适合处理该事件的对象,对于触摸事件,该对象往往是被触摸的视图;对于其他事件,该对象往往是第一响应者。

二. 通过hit-testing找被触摸的视图

iPhone操作系统通过 hit-testing 过程来找出被触摸的视图。该过程会检查触摸点是否位于相关视图的范围之内。如果触摸点位于视图范围内,则进一步对其子视图执行检查过程,最终,触摸点所在的视图层级最底层的视图(对于界面来说是最上层的视图)将成为 hit-test 视图,iPhone操作系统会将触摸事件传递给该视图进行处理。

图一

例如,图一的视图层级为:View A是最底层的视图,先添加View B, 再添加View C; 接着在View C上添加View D, 接着添加View E.

场景一:用户触摸了View B. iPhone操作系统按照如下过程查找hit-test视图

  1. 判断触摸点是否位于 View A 内,若是则进一步检查子视图 View B 和 View C;

  2. 基于“同一层级的视图,后添加地先被检查”的原则,先检查触摸点是否在View C上,发现并不在;

  3. 检查View B, 发现触摸点在其范围内,并且ViewB是触摸点在的视图中最表层的视图,因此它就是hit-test 视图。

场景二:用户触摸了View E. iPhone操作系统按照如下过程查找hit-test视图

  1. 判断触摸点是否位于 View A 内,若是则进一步检查子视图 View B 和 View C;

  2. 基于“同一层级的视图,后添加地先被检查”的原则,先检查触摸点是否在View C上,发现确实是在 View C 内,因此进一步检查子视图;

  3. View E是后添加在View C上,因此先被检查。发现触摸点确实是在View E 上, 并且View E是视图中最表层的视图,因此它就是 hit-test 视图。

图二

例如,图二的视图层级为:View A是最底层的视图,View B是View A的子视图, View C是View B的子视图。

场景一:用户触摸了View C(未与View B重合的部分). iPhone操作系统按照如下过程查找hit-test视图:

  1. 判断触摸点是否位于 View A 内,若是则进一步检查子视图 View B ;

  2. 判断触摸点不再View B内,因此也不再检查View C. View A就被视作hit-test视图.

Tips: 如果某个子视图的一部分位于父视图范围之外,在父视图的 clipsToBounds 属性关闭的情况下,超出父视图范围的这部分子视图不会被裁剪掉,但是此时触摸该子视图位于父视图之外的部分将没有任何反应(接下来会结合hit-testing来具体说明)。

上述查找过程主要利用 hitTest(_:withEvent:) 方法,该方法会根据给定的 CGPoint 和 UIEvent 返回 hit-test 视图。

首先,该方法会向接收者发送 pointInside(_:withEvent:) 消息,如果传入的 CGPoint(即触摸点)不在接收者(也就是某个视图)的范围内,pointInside(_:withEvent:) 将会返回 false, hitTest(_:withEvent:) 就会直接返回 nil,不会进一步检查子视图。

如果传入的 CGPoint(即触摸点)位于接收者(也就是某个视图)的范围内,pointInside(_:withEvent:) 将会返回 true。然后,按照子视图添加顺序的相反顺序(即先添加的子视图后检查触摸点是否在当前视图范围内),对每个子视图发送 hitTest(_:withEvent:) 消息,进一步检查子视图。如上递归过程结束后,最初调用的 hitTest(_:withEvent:) 方法将会返回 hit-test 视图(hit-test 视图在hitTest(_:withEvent:) 中返回自己,其父视图将其返回给上一级父视图...)。如上所述的递归判断过程类似下面这样:

hit-test 视图拥有最先处理触摸事件的机会,之后,还可以选择将触摸事件沿响应者链条传递给下一个响应者,例如 hit-test 视图的父视图。默认情况下,触摸事件被处理后不会传递给下一个响应者。

三.由响应者组成的 响应者链条

UIResponder 类及其子类的对象,都可以被称之为响应者对象,它们具有响应和处理事件的能力。UIApplication、 UIViewController 以及 UIView 都是 UIResponder 的子类,这意味着所有视图控制器和视图都是响应者对象。不过, CALayer 直接继承自 NSObejct,因此它不是响应者对象。

响应者链条即由这样一系列响应者对象链接在一起, 开始于第一响应者对象,结束于 UIApplication 单例对象(实际上往往结束于它的代理对象)。如果第一响应者无法处理事件,事件就会沿响应者链条往下传递,即传递给下一个响应者, 从而传递事件。 

第一响应者即是第一个有机会处理事件的响应者对象。通常情况下,它是一个视图。一个响应者对象若想成为第一响应者,需满足如下两点:

  1. 重写 canBecomeFirstResponder() 方法并返回 true。UIResponder 的默认实现是返回 false。

  2. 收到 becomeFirstResponder() 消息。在某些情况下,往往会手动给响应者发送此消息,从而主动成为第一响应者。

除了传递事件,响应者链条还会传递一些别的东西,具体说来,响应者链条可传递如下事件或者消息:

触摸事件

要处理触摸事件,响应者可以重写 touchesBegan(_withEvent:) 系列方法。

-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event;
-(void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event;
-(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event;
-(void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event;

如果 hit-test 对象不处理触摸事件,触摸事件将沿着响应者链条传递给下一响应者。

运动事件

要处理运动事件,响应者需要重写 motionBegan(_:withEvent:) 系列方法。

-(void)motionBegan:(UIEventSubtype)motionwithEvent:(UIEvent*)event;
-(void)motionEnded:(UIEventSubtype)motionwithEvent:(UIEvent*)event;
-(void)motionCancelled:(UIEventSubtype)motionwithEvent:(UIEvent*)event;

如果第一响应者不处理事件,运动事件将沿着响应者链条传递给下一响应者。

远程控制事件

要处理远程控制事件,响应者需要实现 remoteControlReceivedWithEvent(_:) 方法。在 iOS 7.1 之后,最好使用 MPRemoteCommandCenter 获取多媒体控制操作对应的 MPRemoteCommand 对象,并注册相应的回调方法或者 block。

动作消息

当用户操作某个控件后,例如按钮或者开关,如果该控件的 target 为 nil,那么控件绑定的方法将会从控件开始,沿着响应者链向下传递,寻找一个实现了相应方法的响应者。

编辑菜单消息

当用户点击了编辑菜单的某个命令后,iOS 通过响应者链条来寻找一个实现了相应方法(例如 cut(_:), copy(_:),paste(:_))的响应者。

文本编辑

当用户点击了 UITextField 或者 UITextView 后,该控件会自动成为第一响应者,弹出虚拟键盘并成为输入焦点。

当用户点击 UITextField 或者 UITextView 后,它们会自动成为第一响应者,而其他响应者则需通过接收 becomeFirstResponder() 消息来成为第一响应者。

四.事件在响应者链条上的传递路径

如果某个应该处理事件的响应者(无论是 hit-test 视图还是第一响应者)不处理事件,UIKit 就会将事件沿着响应者链条向下传递,直到找到处理事件的响应者,或者再没有下一个响应者。每个响应者都可以决定是否处理传给自己的事件,以及是否将事件传递给下一个响应者,即 nextResponder 属性所引用的响应者。

iOS的响应链

如左图所示,事件按如下路径进行传递:

  1. 绿色的 initial view 优先处理事件,如果不处理事件,就将事件传递给父视图,因为它并非视图控制器所属的视图。

  2. 黄色的视图有机会处理事件,如果不处理事件,它也将事件传递给父视图,因为它也不是视图控制器所属的视图。

  3. 蓝色的视图有机会处理事件,如果不处理事件,因为它是视图控制器所属的视图,它会将事件传递给视图控制器,而非自己的父视图。

  4. 视图控制器有机会处理事件,如果不处理事件,它将事件传递给自己的视图的父视图,在这种情况下即是主窗口。

  5. 主窗口有机会处理事件,如果不处理事件,它将事件传递给 UIApplication 单例对象。

  6. UIApplication 单例对象有机会处理事件,如果不处理事件,并且应用代理也是 UIResponder 子类,它会将事件传递给应用代理。

  7. 如果到最后也没有响应者处理事件,事件就会被丢弃。

对于右图所示,事件传递过程大同小异,只不过靠左的视图控制器的视图的父视图不是主窗口而是另一个视图控制器的视图,因此它将事件传递给靠右的视图控制器的视图。

如上所述,事件传递的规律是,一个视图将事件传递给父视图,如果自己是视图控制器的视图,则先传给视图控制器,再传递给父视图。主窗口将事件传给 UIApplication 单例对象,后者再进一步传递给 UIApplicationDelegate,前提是应用代理是 UIResponder 的子类。

Tips:  前面提到的“不处理事件”,是指响应者没有对 UIResponder 的相应事件处理方法进行重写。例如,要处理触摸事件,需重写 touchesBegan(_:withEvent:) 系列方法。虽然 UIResponder 的默认实现为空,但是它的某些子类,例如 UIView、UIWindow、UIViewController 和 UIApplication,默认会将事件传递给下一响应者。对于这些子类,重写事件处理方法后,若想继续传递事件,应该调用超类方法,而不是通过 nextResponder 拿到下一响应者来手动调用其相关方法。

作者:瓷月亮

链接:https://www.jianshu.com/p/e84e3f47a040

(0)

本文由 投稿者 创作,文章地址:https://blog.isoyu.com/archives/iosdeshijianchuandi-xiangyingzheliantiao.html
采用知识共享署名4.0 国际许可协议进行许可。除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为:7 月 18, 2018 at 07:22 下午

热评文章

发表回复

[必填]

我是人?

提交后请等待三秒以免造成未提交成功和重复