IOS基础知识-事件传递与响应链原理篇

iOS中事件一共有四种类型,包含触摸事件,运动事件,远程控制事件,按压事件;

UIEvent
UIEvent描述了单次的用户与应用的交互行为,例如触摸屏幕会产生触摸事件,晃动手机会产生运动事件。UIEvent对象中记录了事件发生的时间,类型,对于触摸事件,还记录了一组UITouch对象,下面是UIEvent的几个属性:

@property(nonatomic,readonly) UIEventType     type NS_AVAILABLE_IOS(3_0);  //事件的类型
@property(nonatomic,readonly) UIEventSubtype  subtype NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) NSTimeInterval  timestamp;  //事件的时间
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;  //事件包含的touch对象

UITouch
UITouch记录了手指在屏幕上触摸时产生的一组信息,包含触摸的时间,位置,所在的窗口或视图,触摸的状态,力度等信息

@property(nonatomic,readonly) NSTimeInterval      timestamp;  //时间
@property(nonatomic,readonly) UITouchPhase        phase;  //状态,例如begin,move,end,cancel
@property(nonatomic,readonly) NSUInteger          tapCount;   // 短时间内单击的次数
@property(nonatomic,readonly) UITouchType         type NS_AVAILABLE_IOS(9_0);  //类型
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);  //触摸半径
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);
@property(nullable,nonatomic,readonly,strong) UIWindow                        *window;  //触摸所在窗口
@property(nullable,nonatomic,readonly,strong) UIView                          *view;  //触摸所在视图
@property(nullable,nonatomic,readonly,copy)   NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);  //正在接收该触摸对象的手势识别器
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);  //触摸的力度

每一根手指的触摸都会产生一个UITouch对象,多个手指触摸便会有多个UITouch对象,当手指在屏幕上移动时,系统会更新UITouch的部分属性值,在触摸结束后系统会释放UITouch对象。
当事件产生后,系统会寻找可以响应该事件的对象来处理事件,如果找不到可以响应的对象,事件就会被丢弃。那么哪些对象可以响应事件呢?只有继承于UIResponder的对象才能够响应事件,UIApplication,UIView,UIViewcontroller均继承于UIResponder,因此它们能够响应事件。UIResponder提供了响应事件的一组方法:

- (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; //触摸被中断,例如触摸时电话呼入

如果我们想要对事件进行自定义的处理(比如手指在屏幕滑动时让某个view跟着移动),我们需要重写以上四个方法,对于UIViewcontroller,我们只需要在UIViewcontroller中重写上面四个方法,对于UIView,我们需要创建继承于UIView的子类,然后在子类中重写上面的方法,这点需要注意

事件的传递

事件产生之后,会被加入到由UIApplication管理的事件队列里,接下来开始自UIApplication往下传递,首先会传递给主window,然后按照view的层级结构一层层往下传递,一直找到最合适的view(发生touch的那个view)来处理事件。查找最合适的view的过程是一个递归的过程,其中涉及到两个重要的方法 hitTest:withEvent:和pointInside:withEvent:
当事件传递给某个view之后,会调用view的hitTest:withEvent:方法,该方法会递归查找view的所有子view,其中是否有最合适的view来处理事件,整个流程如下所示:


4756715-89f3152cac96cad0.png

hitTest:withEvent:代码实现:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //首先判断是否可以接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    //然后判断点是否在当前视图上
    if ([self pointInside:point withEvent:event] == NO) return nil;
    //循环遍历所有子视图,查找是否有最合适的视图
    for (NSInteger i = self.subviews.count - 1; i >= 0; i--) {
        UIView *childView = self.subviews[i];
        //转换点到子视图坐标系上
        CGPoint childPoint = [self convertPoint:point toView:childView];
        //递归查找是否存在最合适的view
        UIView *fitView = [childView hitTest:childPoint withEvent:event];
        //如果返回非空,说明子视图中找到了最合适的view,那么返回它
        if (fitView) {
            return fitView;
        }
    }
    //循环结束,仍旧没有合适的子视图可以处理事件,那么就认为自己是最合适的view
    return self;
}

pointInside:withEvent:方法作用是判断点是否在视图内,是则返回YES,否则返回NO
判断一个view是否能够接收事件有三个条件,分别是,是否禁止用户交互(userInteractionEnabled = NO),是否被隐藏(hidden = YES)以及透明度是否小于等于0.01(alpha <=0.01)
从递归的逻辑我们知道,如果触摸的点不在父view上,那么其上的所有子view的hitTest都不会被调用,需要指出的是,如果子view尺寸超出了父view,并且属性clipsToBounds设置为NO,触摸发生在子view超出父view的区域内,依旧不返回子view。反过来,如果触摸的点在父view上并且父view就是最合适的view,那么它的所有子view的hitTest还是会被调用,因为如果不调用无法知道是否还有比父view更合适的子view存在。

事件的响应

在找到最合适的view之后,会调用view的touches方法对事件进行响应,如果没有重写view的touches方法,touches默认的做法是将事件沿着响应者链往上抛,交给下一个响应者对象。也就是说,touches方法默认不处理事件,只是将事件沿着响应者链往上传递。那么响应者链是什么呢?

响应者链
在应用程序中,视图放置都是有一定层次关系的,点击屏幕之后该由下方的哪个view来响应需要有一个判断的方式。响应者链是由一系列可以响应事件的对象(继承于UIResponder)组成的,它决定了响应者对象响应事件的先后顺序关系。下图展示了UIApplication,UIViewcontroller以及UIView之间的响应关系链:


4756715-ffa14e1778d83136.png

响应者链在递归查找最合适的view的时候形成,所找到的view将成为第一响应者,会调用它的touches方法来响应事件,touches方法默认的处理是将事件往上抛给下一个响应者,而如果下一个响应者的touches方法没有重写,事件会继续沿着响应者链往上走,一直到UIApplication,如果依旧不能处理事件那么事件就被丢弃。

UIView
如果view是viewcontroller的根view,那么下一个响应者是viewcontroller,否则是super view

UIViewcontroller
如果viewcontroller的view是window的根view,那么下一个响应者是window;如果viewcontroller是另一个viewcontroller模态推出的,那么下一个响应者是另一个viewcontroller;如果viewcontroller的view被add到另一个viewcontroller的根view上,那么下一个响应者是另一个viewcontroller的根view

UIWindow
UIWindow的下一个响应者是UIApplication

UIApplication
通常UIApplication是响应者链的顶端(如果app delegate也继承了UIResponder,事件还会继续传给app delegate)

手势识别器工作机制

常见的手势包括单击、拖动,长按,横扫或竖扫,缩放,旋转等,另外我们还可以创建自定义的手势。
UIGestureRecognize是手势识别器的父类,所有具体的手机识别器均继承于该父类,如果我们自定义手势,也需要继承该类。该类并没有继承于UIResponder,所以手势识别器并不参与响应者链。那么手势识别器是如何工作的呢?

当触摸屏幕产生touch事件后,UIApplication会将事件往下分发,如果视图绑定了手势识别器,那么touch事件会优先传递给绑定在视图上的手势识别器,然后手势识别器会对手势进行识别,如果识别出了手势,就会调用创建手势时所绑定的回调方法,并且会取消将touch事件继续传递给其所绑定的视图,如果手势识别器没有识别出对应的手势,那么touch事件会继续向手势识别器所绑定的视图传递。
虽然手势识别器并不是响应者链中的一员,但是手势识别器会观察touch事件,并延迟事件向所绑定的视图传递,这短暂的延迟使手势识别器有机会优先去识别手势处理touch事件。
对于UIKit提供的的标准控件,可以很方便地通过Target-Action的方式增加事件处理逻辑,默认情况下,发生在标准控件上的touch事件会优先被标准控件通过target-action方式处理,而不会去响应手势。举个例子,如果视图上绑定了单击的手势识别器,然后视图上又添加了一个UIButton,button通过target-action的方式设置了点击执行的操作,那么当点击button时,响应的是button的点击事件,而不是父视图上的单击手势。如果希望手势识别器优先标准控件的target-action进行事件处理,那么可以直接在标准控件上绑定手势识别器,比如上例,如果直接在button上绑定了单击手势,那么响应的就是单击手势了;

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

推荐阅读更多精彩内容

  • 前言: 按照时间顺序,事件的生命周期是这样的: 事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的vi...
    reviewThis阅读 720评论 1 2
  • 系统响应阶段 1.手指触碰屏幕,屏幕感受到触摸后,将事件交由IOKit来处理。 2.IOKIT将触摸事件封装成IO...
    荒漠现甘泉阅读 978评论 1 4
  • 本文主要讲解iOS触摸事件的一系列机制,涉及的问题大致包括: 触摸事件由触屏生成后如何传递到当前应用? 应用接收触...
    baihualinxin阅读 1,192评论 0 9
  • 在iOS开发中经常会涉及到触摸事件。本想自己总结一下,但是遇到了这篇文章,感觉总结的已经很到位,特此转载。作者:L...
    WQ_UESTC阅读 5,987评论 4 26
  • 时光披着锦衣,在我眼前微笑,张开双臂,想把它抱在胸前,它却如闪电划过夜空,没等我反应过来,已经消失得无影无踪。 于...
    秋之语阅读 249评论 0 1