# iOS开发:剖析UITouch传递链和响应链

Posted wuwuFQ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了# iOS开发:剖析UITouch传递链和响应链相关的知识,希望对你有一定的参考价值。

每一次温柔或强烈的触摸,都会上演一场你争我抢的小电影!

故事背景:

UITouch:(起因)

  • 一个手指一次触摸屏幕,就对应生成一个UITouch对象。多个手指同时触摸,生成多个UITouch对象。
  • 多个手指先后触摸,系统会根据触摸的位置判断是否更新同一个UITouch对象。若两个手指一前一后触摸同一个位置(即双击),那么第一次触摸时生成一个UITouch对象,第二次触摸更新这个UITouch对象(UITouch对象的 tap count 属性值从1变成2);若两个手指一前一后触摸的位置不同,将会生成两个UITouch对象,两者之间没有联系。
  • 每个UITouch对象还包含指示触摸发生时间的时间戳、表示用户点击屏幕次数的整数以及描述触摸是开始、移动还是结束的常量形式的触摸阶段,或者系统是否取消了触摸。
//触摸的各个阶段状态 
//例如当手指移动时,会更新phase属性到UITouchPhaseMoved;手指离屏后,更新到UITouchPhaseEnded
typedef NS_ENUM(NSInteger, UITouchPhase) 
    UITouchPhaseBegan,             // whenever a finger touches the surface.
    UITouchPhaseMoved,             // whenever a finger moves on the surface.
    UITouchPhaseStationary,        // whenever a finger is touching the surface but hasn't moved since the previous event.
    UITouchPhaseEnded,             // whenever a finger leaves the surface.
    UITouchPhaseCancelled,         // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
;

UIEvent:(要做的事)

  • 触摸的目的是生成触摸事件供响应者响应,一个触摸事件对应一个UIEvent对象,其中的 type 属性标识了事件的类型(触摸事件,加速计事件、远程控制事件)。

  • 触摸事件对象包含与事件有某种关系的触摸(即屏幕上的手指)。一个触摸事件对象可能包含一个或多个触摸,每个触摸由一个UITouch对象表示。当触摸事件发生时,系统会将其路由到适当的响应者并调用适当的方法,例如 touchesBegan(_:with:)

  • 应用程序可以接收许多不同类型的事件,包括触摸事件、运动事件、遥控事件和按压事件。触摸事件是最常见的,并被传递到最初发生触摸的视图。运动事件是 UIKit 触发的,与 Core Motion 框架报告的运动事件是分开的。远程控制事件允许响应者对象接收来自外部附件或耳机的命令,以便它可以管理音频和视频——例如,播放视频或跳到下一个音轨。按下事件表示与游戏控制器、AppleTV 遥控器或其他具有物理按钮的设备的交互。您可以使用typesubtype属性确定事件的类型。

UIResponder:(谁去做这件事)

每个响应者都是一个UIResponder对象,即所有派生自UIResponder的对象,本身都具备响应事件的能力。因此以下类的实例都是响应者:

  • UIView
  • UIViewController
  • UIWindow
  • UIApplication

响应者之所以能响应事件,因为其提供了4个处理触摸事件的方法:

//手指触碰屏幕,触摸开始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移动
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指离开屏幕,触摸结束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//触摸结束前,某个系统事件中断了触摸,例如电话呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

那么,咱们现在知道了,有一个手指触摸了屏幕生成了触摸对象UITouch,并产生了触摸事件UIEvent,那么让哪个视图(UIResponder)去处理这次事件呢?后宫嫔妃众多,这次不可能雨露均沾,只能翻牌决定了!

因此这就存在一个寻找事件最佳响应者(又称第一响应者 First Responder)的过程,目的是找到一个具备最高优先级响应权的响应对象(the most appropriate responder object),这个过程叫做Hit-Testing(命中测试),那个命中的最佳响应者称为hit-tested view。


传递链

寻找第一响应者所形成的链表我们称为事件传递链
传递链用到的主要函数:hitTest:withEvent:pointInside:withEvent:
寻找命中测试视图或者叫第一响应者的条件是:

- alpha > 0.01
- userInteractionEnable = YES
- hidden = NO
- 触摸点在视图坐标内(视图不可超出父控件区域)
  • 点击屏幕产生触摸事件,系统将这个事件加入到一个由UIApplication管理的事件队列中,UIApplication会从消息队列里取事件分发下去,首先传给UIWindow
  • 在UIWindow中就会调用hitTest:withEvent:方法去返回一个最终响应的视图
  • 在hitTest:withEvent:方法中就会去调用pointInside: withEvent:去判断当前点击的point是否在UIWindow范围内,如果是的话,就会去遍历它的子视图来查找最终响应的子视图
  • 遍历的方式是使用倒序的方式来遍历子视图,也就是说最后添加的子视图会最先遍历,在每一个视图中都回去调用它的hitTest:withEvent:方法,可以理解为是一个递归调用
  • 最终会返回一个响应视图,如果返回视图有值,那么这个视图就作为最终响应视图,结束整个事件传递;如果没有值,那么就会将UIWindow作为响应者

响应链

  • 我们已经知道寻找第一响应者所形成的是传递链,那么按照传递链的反向路径由各个响应者所组成链表叫做响应链
  • 传递链的目的是找到第一响应者,而这个过程形成了一条有效的响应者路径;响应链的目的是找到最终响应者

再通俗一点:

  • 手指触摸屏幕上的一点,谁来处理这个事件,那么我们就要找点击在了哪些视图的有效范围内,从UIwindow窗口开始找,一直找到最靠近用户的视图(也就是最外层),自下而上找到了可以响应这个点击的所有视图,这些响应者视图组成了传递链;
  • 传递链上的视图都是响应者,响应者可以响应事件,但可能没有添加点击事件或者没有实现 touchesBegan: 等方法,就无法处理事件,接下来就是寻找谁能处理这次的事件,按照响应链来依次响应,都没有响应处理,那么这次点击就当做一次误触处理,不作任何反应。

个人理解总结


上图的视图结构是:橘色 --> 灰色(黄色) --> 红色(绿色) --> 蓝色。()内是同级视图,是后添加的。
我们给灰色视图添加一个点击事件,然后手指点击蓝色视图。打印如下:

上面的日志是在系统 (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 里面打印的,看打印顺序,你可能就明白我之前讲的一大堆了,打印的日志就是寻找第一响应者的过程:从底部开始,倒序循环子视图,最后找到了蓝色是第一响应者,再反方向,按照响应链,找到了灰色视图。

通透了吧! Nice!
The End


以上是关于# iOS开发:剖析UITouch传递链和响应链的主要内容,如果未能解决你的问题,请参考以下文章

iOS响应链和传递机制

iOS开发 - 事件传递响应链

iOS开发 - 事件传递响应链

iOS开发UITouch触摸API简介

iOS开发UITouch触摸API简介

iOS 手势操作和事件传递响应链