iOS的事件和响应者链

之前面试问到一个响应者链的问题,结果让我很尴尬。于是,就想着写篇关于响应链的总结。当然,响应者链也包含事件、响应者的知识点,所以就一起总结复习一下。

一. 事件(UIEvent)

一个UIEvent对象代表iOS中的事件(简单理解,事件就是用户对设备的操作)。事件分为三类:触摸事件、晃动事件、远程控制事件(比如耳机按钮操控)。

要学习UIEvent之前,我们先来简单了解一下UITouch(你可以理解为触碰点)。
当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象,一根手指对应一个UITouch对象。UITouch的作用是保存着跟手指相关的信息,比如触摸的位置、时间、阶段。当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置,当手指离开屏幕时,系统会销毁相应的UITouch对象。

UITouch常用属性和方法有:

@property(nonatomic,readonly) NSTimeInterval      timestamp;//记录了触摸事件产生或变化时的时间
@property(nonatomic,readonly) UITouchPhase        phase;//当前触摸事件所处的状态
@property(nonatomic,readonly) NSUInteger          tapCount; // 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nullable,nonatomic,readonly,strong) UIWindow   *window;//触摸产生时所处的窗口
@property(nullable,nonatomic,readonly,strong) UIView    *view;//触摸产生时所处的视图

//触碰点在所处view的位置(以view的左上角为原点(0, 0))
//调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)locationInView:(nullable UIView *)view;
//取得移动的前一个位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;

以常见的触摸事件为例:
一个触摸事件包含一个或者多个手指,每个手指是一个UITouch对象;每产生一个事件,就会产生一个UIEvent对象,用于记录事件产生的时刻和类型。
一次完整的触摸过程,会经历3个状态(开始,移动,结束。也可能会有取消)

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

4个触摸事件处理方法中,都有touches和event两个参数。一次完整的触摸过程中,只会产生一个事件对象,所以4个触摸方法都是同一个event参数。如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象。简单点说,一次触摸事件就是一个event,touches的数量取决于你用了几个手指。

同样的,我们也看看UIEvent常用属性和方法,最主要的还是关注里面的touches:

@property(nonatomic,readonly) UIEventType     type;
@property(nonatomic,readonly) UIEventSubtype  subtype;
@property(nonatomic,readonly) NSTimeInterval  timestamp;
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;

- (nullable NSSet <UITouch *> *)allTouches;
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;

二. 响应链(UIResponder Chain)

在讲响应链之前,我们先来讲讲事件的处理机制(事件的传递)。

有这么一个问题:当我们触碰屏幕时,会产生一个事件(UIEvent),系统是怎么找到查找事件触发者?(或者这么想:当我们触碰屏幕时,程序是怎么知道我们在触碰哪一个控件的?),这就涉及到事件的分发。
事件的传递涉及到了UIView中的两个方法:

//询问当前点击事件最优响应者是谁(nil为没有最优响应者)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

//判断当前点击是否在控件的Bounds之内(用来判断某个view能不能成为最优响应者)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

在iOS中发生触摸后,事件会加入UIApplication事件队列,UIApplication会从事件队列取出最前面的事件并分发处理,通常,先发送事件给应用程序的主窗口(UIWindow),主窗口会调用hitTest:withEvent:方法在视图(UIView)层次结构中找到一个最合适的UIView(事件触发视图)来处理触摸事件。

具体步骤如下:
① 在顶级视图(key window的视图)上调用pointInside:withEvent:方法判断触摸点是否在当前视图内。

② 如果返回NO,那么hitTest:withEvent:返回nil(说明这个点都不在我的视图里,我这边肯定没有你的最优响应者)。

③ 如果返回YES,那么它会向当前视图的所有子视图(key window的子视图)发送hitTest:withEvent:消息(我这边有最优响应者,要嘛是我,要嘛是我的子视图,我再帮你找找)。遍历所有子视图的顺序是从subviews数组的末尾向前遍历(从界面最上方开始向下遍历)。

④ 如果subview(没有子视图了)的hitTest:withEvent:返回非空对象则顶级视图的hitTest:withEvent:也返回此对象,处理结束(注意所有视图的hitTest:withEvent:都是根据pointInside:withEvent:的返回值来确定是返回空还是当前子视图对象的。如果该视图的hidden=YESuserInteractionEnabled=NO或者alpha<0.1都会直接返回nil)。

⑤ 如果所有subview遍历结束仍然没有返回非空对象,则顶级视图的hitTest:withEvent:返回它自己(儿子都不是最优响应视图,只能我这当爹的来了)。

具体的一个伪代码就是如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    //先判断alpha,userInteractionEnabled,hidden
    if (self.alpha < 0.01 || !self.userInteractionEnabled || self.hidden) {
        return nil;
    }
    //再判断是不是在我的范围内
    if (![self pointInside:point withEvent:event]) {
        return nil;
    }
    
    //在我的范围内,我就问我的儿子,有一个儿子回答是它,我就返回它,如果都没人回答,我就回答是我了。
    __block UIView *hitView = nil;
    [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) {
        hitView = [subview hitTest:point withEvent:event];
        if (hitView) {
            *stop = YES;
        }
    }];
    return hitView ? : self;
}
事件的传递.png

知道事件怎么传递的,那再让我们来探探我们的重点:响应者链

我们知道在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。
当然,在iOS中不是所有的对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件,称之为响应者对象,比如UIApplication、UIViewController、UIView都继承自UIResponder。


响应者链.png

之前提到的事件的传递,其实就是在找事件触发者的过程。但是事件触发者(触摸对象)并非就是事件的响应者。
比如这么一个例子:在视图控制器放一个UIImageView,通过touchesMoved:来控制imageView的位置。

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    //取得一个触摸对象(对于多点触摸可能有多个对象)
    UITouch *touch=[touches anyObject];
    //NSLog(@"%@",touch);
    
    //取得当前位置
    CGPoint current=[touch locationInView:self.view];
    //取得前一个位置
    CGPoint previous=[touch previousLocationInView:self.view];
    
    //移动前的中点位置
    CGPoint center=_image.center;
    //移动偏移量
    CGPoint offset=CGPointMake(current.x-previous.x, current.y-previous.y);
    
    //重新设置新位置
    _image.center=CGPointMake(center.x+offset.x, center.y+offset.y);
}

你会发现我们即使在imageView上移动,也会执行视图控制器的touchesMoved:,这也意味着这时的响应者是视图控制器,而不是触摸对象imageView。这也说明了触摸对象imageView不自己处理事件,把它转移给视图控制器。为什么会这样呢?
其实,当某个视图的属性满足这样条件时,意味着它不处理事件,会把事件转移给响应者链的下一个去处理。对于视图控制器这种,你没有实现开始触摸方法,就意味着你不处理事件。

  • userInteractionEnabled = NO
  • hidden = YES
  • alpha = 0~0.01
  • 没有实现开始触摸方法(只针对视图控制器这种类型的响应者)

例子中的imageView的userInteractionEnabled默认为NO,所以会把事件转移给响应者链的下一个(self.view),self.view的userInteractionEnabled默认为NO,也不处理事件,就继续转移给下一个(ViewController),刚好ViewController实现了触摸方法,可以处理事件。要是ViewController也没用实现了触摸方法的话,就会继续传递下去。
要是例子中的imageView换成button就不一样了,button的userInteractionEnabled默认为YES,自己就能处理事件,就没有ViewController什么事了(也就是touchesMoved:不会执行了)。

所以,一个完整的流程是这样的:
① 当一个事件发生后首先看initial view(触摸对象)能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView)。

② 如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传递。(对于视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理)。

③ 一直到window,如果window还是不能处理此事件则继续交给application(UIApplication单例对象)处理,如果最后application还是不能处理此事件则将其丢弃。

三. 总结

以常见的触摸操作来说,我们来串一下:

事件

我们主要知道事件(UIEvent)就是保存这次触摸的信息(时间,手指,类型等),当然手指的信息(位置,所处视图,点击次数等)是UITouch来保存。一次触摸从开始到结束就是一个事件,不管你用几个手指。

响应者链

触碰操作产生一个事件,事件是通过UIApplication分发找到对应的最优响应者(触摸对象)。找到归找到,又不一定是这个触摸对象来处理事件,要是你不能处理,你就交给你的上级(响应者链的上一级),直到有人处理,或者都没人处理。

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

推荐阅读更多精彩内容