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 拿到下一响应者来手动调用其相关方法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容