iOS事件传递和事件响应机制

这里主要讲解记录下用户触摸点击手机屏幕后产生的事件是如何派发传递的,如何查找到适合响应事件的第一响应者控件,以及找到响应者后事件是如何通过响应链向下传递的,直到事件被接收并做出具体处理或被废弃。事件响应链也是面试中经常会被问起的知识点!

一、相关概念

  • 第一响应者:

第一响应者一般指的是用户当前触摸的响应者对象,表示当前该对象正在与用户交互,第一响应者是响应者链的开端。

响应者链和事件分发传递的使命都是找出第一响应者

  • 响应者对象:

具有响应和处理iOS事件能力的对象,也就是继承UIResponder的类的对象。我们常用的UIApplication、UIWindow、UIViewController、UIView、UIScene(iOS13以后)都是继承或间接UIResponder类,所以他们的实例对象都可以成为响应者对象。

类的继承关系:

图片
  • 响应者链:

由多个不同响应者对象链接起来构成的一个链条;

响应者链可以看做是链表,整体是一个树,因为每个节点都是一个响应者对象,每个响应者对象都存有指向下一个响应者的指针nextResponder,可以通过nestResponder找到下一个responder,直到找到第一响应者响应了事件就会停止传递,如果最终没有响应者响应事件,那么该事件就会被废弃。

二、iOS中的事件类型:

iOS中事件主要分为三大类:

1、Touch Event (触摸事件)

解释:用户触摸屏幕产生的交互事件

2、Motion Event (运动事件)

解释:运动事件也叫做加速计事件,这类事件是依赖手机里的加速计、陀螺仪等硬件传感器实现的。用户在摇晃手机、倾斜手机的售后就会产生这类事件。可用于屏幕转屏监控。

3、Remote-ControlEvent(远程控制事件)

解释:这个事件指的是用户在操作多媒体的时候产生的事件。例如播放音乐时后台播放控制

三、如何控制控件能不能响应事件:

  1. 设置不允许交互:设置控件的userInteractionEnabled = NO;
  2. 设置控件隐藏:将控件的hidden设置为Yes隐藏控件;
  3. 设置透明度:设置控件的透明度alpha<0.01,放alpha的值在0.0~0.01之间时控件为透明;
  4. 超出父控件响应区域

注意:如果view被设置为透明,那么会直接影响其子View的透明度;如果view无法响应事件那么这个view上的所有SubView都不可响应事件,也就是如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件。

四、事件的产生和分发传递:

测试展示图

图片
  • 事件是如何产生的?

当用户触摸屏幕时,系统会检测到屏幕上的压力感知到触摸事件,iOS系统检测到触摸操作后会将这个事件打包成一个UIEvent对象,并将该事件加入到一个由UIApplication管理的事件队列中,然后UIApplication会从事件队列中取出触摸事件并传递给UIWindow处理,keyWindow会使用hitTest:withEvent:方法寻找一个最合适的响应者来处理事件,一般寻找到的适合处理事件的控件是touch操作初始点的视图,找到合适第一响应者的视图控件后,就会调用该视图控件的touches方法来处理具体的事件,这个过程称之为hit-test。

  • 处理事件的方法:
UIResponder内部提供了以下方法来处理事件触摸事件
- (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;
  • 事件是如何传递的?

事件的传递是由父控件向子控件传递的,例如上面的view层次图,viewA、viewB、viewE被添加到rootView中,viewC、viewD是viewB的子view。加入用户点击viewC的时间传递链是

图片

传递方向:由底层系统向可以响应事件的控件传递

UIKit→UIApplication的事件队列→keyWindow→rootView→一些列subView→事件响应view

  • 如何查找到合适的事件第一响应者?

主要方法:

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

注:只要事件传递给一个控件,那么这个控件就会调用自己的hitTest:withEvent:方法。他的作用是寻找并返回适合响应处理事件的第一响应者。

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

注:作用是判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

流程图:

图片
  1. 主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口的范围内
  2. 如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件,遍历自己的子控件只是为了寻找出来最合适的view;
  3. 遍历到每一个子控件后,又会重复上面的两个步骤。将传递事件给子控件,先判断子控件能否接受事件,再判断触摸点在不在子控件的范围中;
  4. 如此循环遍历子控件,直到找出合适响应事件的第一响应者,如果没有更合适的子控件,那么自己就成为最合适的view。
  • hitTest:withEvent方法中如何处理的?
  1. 首先判断当前视图是否可响应事件,也就是判断当前视图的是否可交互状态、隐藏状态、透明度;
  2. 如果当前视图允许响应触摸事件,则调用当前视图的 pointInside:withEvent: 方法判断触摸点是否在当前视图内 ;
  3. 若pointInside:withEvent:返回NO,则 hitTest:withEvent: 返回 nil ;
  4. 若pointInside:withEvent:返回 YES,则向当前视图的所有子视图发送 hitTest:withEvent: 消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从 subviews 数组的末尾向前遍历 ,直到有子视图返回非空对象或者全部子视图遍历完毕;
  5. 若第一次有子视图返回非空对象,则 hitTest:withEvent: 方法返回此对象,处理结束;如所有子视图都返回 nil,则 hitTest:withEvent: 方法返回该视图自身 ;
  6. 找到合适的第一响应者后,就会调用该控件的touches系列方法处理具体的事件,如果找不到第一响应者就不会调用touches方法
  • 查找响应者实例

以测试展示图为例,假设用户点击viewC后的处理流程

  1. rootView为window的根视图,窗口会首先对rootView进行hit-Test,判断结果为用户点击位置在rootView的范围内;
  2. 继续检测rootView的子控件(viewA,viewB,viewE)相应的调用自己的hit-Test方法,检测到viewA、viewE的pointInside:withEvent:返回NO,则点击范围不在viewA、viewE内,对应的hitTest:withEvent:返回nil,这时rootView继续检测viewB的hit-Test方法,viewB的pointInside:withEvent:返回YES,确定点击范围在viewB内;
  3. 这时viewB内存在viewC和viewD两个子控件,viewD在viewC之后添加到viewB的subViews中,因此优先检测viewD的hit-Test方法,viewD的pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil,说明点击不在viewD内,viewD及其子控件都不可响应事件。因此需要回溯检测viewC的hit-Test方法;
  4. viewC的pointInside:withEvent:返回YES,说明点击范围在viewC范围内,由于viewC没有子控件,也可以理解为viewC的子控件hit-Test返回了nil;
  5. 因此viewC的hitTest:withEvent:将会返回viewC,viewB的hitTest:withEvent:返回viewC,rootViewhitTest:withEvent:将会返回viewC;
  6. 至此,本次点击事件的第一响应者就通过响应者链的事件分发逻辑找到了

注意:如果最终hit-test没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯,如果UIWindow实例和UIApplication实例都不能处理该事件,则该事件会被丢弃;

  • hitTest:withEvent:方法底层实现
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// point:是方法调用者坐标系上的点
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    // 1.判断下窗口能否接收事件
     if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil; 
    // 2.判断下点在不在窗口上 
    // 不在窗口上 
    if ([self pointInside:point withEvent:event] == NO) return nil; 
    // 3.从后往前遍历子控件数组 
    int count = (int)self.subviews.count; 
    for (int i = count - 1; i >= 0; i--)     { 
    // 获取子控件
    UIView *childView = self.subviews[I]; 
    // 坐标系的转换,把窗口上的点转换为子控件上的点 
    // 把自己控件上的点转换成子控件上的点 
    CGPoint childP = [self convertPoint:point toView:childView]; 
    UIView *fitView = [childView hitTest:childP withEvent:event]; 
    if (fitView) {
    // 如果能找到最合适的view 
    return fitView; 
    }
    } 
    // 4.没有找到更合适的view,也就是没有比自己更合适的view 
    return self;
    }

五、事件响应

  • 响应链的传递方向

由是第一响应者的控件向系统传递

事件响应view→superView→rootVIew→viewController→window→Application→AppDelegate

  • 响应者链的关系图:
图片

解释说明:
1、响应者链是由多个响应者对象构成的链条,每个响应者对象必须是继承UIResponder类的子类;
2、如果View是控制器VC的View,那么VC就是view的nextUIResponder;
3、如果View不是控制器VC的View,那么此View的superView为当前view的nextUIResponder;
4、视图控制器VC的nextUIResponder是控制器view(VC.View)的superView,即VC.nextUIResponder = VC.View.superView,如下图;
5、如果在视图层都不能处理事件,则将事件传递个UIWindow进行处理;
6、Window的nextResponder是UIApplication,如果window也不处理事件,则将事件传递给UIApplication;
7、UIApplication的nextResponder是AppDelegate,如果UIApplication也不能处理该事件,则将此事件丢弃

说明示图:

输出的log 显示VC.nextUIResponder = VC.View.superView

图片

六、总结

  1. 当用户点击页面上一个view的时候,系统只是检测到用户点击触摸了屏幕,而此时无法确认用户触摸的view控件,因此需要根据事件分发传递的逻辑寻找到可以响应事件的第一响应者控件;
  2. 如果需要处理特殊的需求,例如单击不规则按钮事件、点击事件穿透等问题时可以重写主要方法hitTest:withEvent:和pointInside:withEvent:来处理;
  3. 发生了触摸或其他事件后,系统将事件打包成UiEvent发送到UIApplication管理的事件队列中,UIApplication从队列中取出最前面的事件分发下去;
  4. 如果找到了合适处理事件的控件,会调用此控件的touchs系列方法,如果响应事件的控件调用了 super touchs等方法,那么事件会沿着响应链向下传递,传递给下一个响应者,这个响应者来调用touchs系列方法;
  5. 如果父视图不接收处理事件,那么他的子视图也不能接收到;
  6. 事件传递是由父控件向子控件传递的,事件响应是由子控件向父控件出啊低的;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345