ios——事件传递与响应者链

一、事件分类

事件是发送到应用程序用于通知用户操作的对象。 在iOS中,事件可以采取多种形式:多点触摸事件,运动事件和用于控制多媒体的事件。 这最后一种类型的事件被称为遥控事件或者远程控制事件,因为它可以源自外部附件。而在我们开发过程中最常用的就是多点触摸事件。

二、事件传递

当用户生成的事件发生时,UIKit创建一个包含处理事件所需信息的事件对象。 然后它将事件对象放置在活动应用程序的事件队列中。 对于触摸事件,该对象是在UIEvent对象中打包的一组触摸(UIEvent中包含了所有UITouch信息)。 对于运动事件,事件对象因您使用的框架和您感兴趣的运动事件类型而异。

事件沿着特定路径传递,直到它被传递到可以处理它的对象。 首先,单例UIApplication对象从队列的顶部获取一个事件并分发处理。 通常,它将事件发送到应用程序的key window对象,该对象将事件传递到初始对象(initial object)进行处理。 初始对象取决于事件的类型。

  • 触摸事件:对于触摸事件,窗口对象首先尝试将事件传递到发生触摸的视图。 该视图称为命中测试视图(hit-test view)。 找到命中测试视图(hit-test view)的过程称为命中测试(hit-testing),这在Hit-Testing返回触摸发生的视图中描述。

  • 运动和遥控事件:对于这些事件,窗口对象将摇动或远程控制事件发送到第一响应者以进行处理。 第一响应者在响应者链由响应者对象组成中描述。

这些事件路径的最终目标是找到一个可以处理和响应事件的对象。 因此,UIKit首先将事件发送到最适合处理事件的对象。 对于触摸事件,该对象是命中测试视图(hit-test view),对于其他事件,该对象是第一个响应者。

{\large\text{作者:坤坤同学 链接:https://www.jianshu.com/p/847432c2cb3b 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。}}

三、命中测试

一根手指触摸屏幕时会创建一个UITouch对象,最终生成UIEvent对象,并通过sendEvent:函数发送给UIWindowkeyWindow)。

  1. UIApplication接收到事件,将事件传递给keyWindow
  2. keyWindow遍历subViewshitTest:withEvent:方法,找到点击区域内合适的视图来处理事件。
  3. UIView的子视图也会遍历其subViewshitTest:withEvent:方法,以此类推。
  4. 直到找到点击区域内,且处于最上方的视图,将视图逐步返回给UIApplication
  5. 在查找第一响应者的过程中,已经形成了一个响应者链。
  6. 应用程序会先调用第一响应者处理事件。
  7. 如果第一响应者不能处理事件,则调用其nextResponder方法,一直找响应者链中能处理该事件的对象。
  8. 然后交给UIApplication后,最后交给UIApplicationDelegate,仍然没有能处理该事件的对象,则该事件被废弃。
  • 这里涉及两条链:
    Hit-Testing链,由系统向命中view传递UIKit –> active app's event queue –> window –> root view –>......–>lowest view
    响应链,由命中view向系统传递initial view –> super view –> .....–> view controller –> window –> Application –> AppDelegate

举例说明:
1.如果点击UITextField后其会成为第一响应者。
2.如果textField未处理事件,则会将事件传递给下一级响应者链,也就是其父视图。
3.父视图未处理事件则继续向下传递,也就是UIViewControllerView
4.如果控制器的View未处理事件,则会交给控制器处理。
5.控制器未处理则会交给UIWindow
6.然后会交给UIApplication
7.最后交给UIApplicationDelegate,如果其未处理则丢弃事件。

案例说明,假设用户触摸下图中的View E。 iOS通过按照此顺序检查子视图来查找命中测试视图(hit-test view):

  1. 触摸在View A的边界内,因此它检查子视图View BView C.

  2. 触摸不在View B的界限内,但它在View C的界限内,因此它检查子视图View DView E.

  3. 触摸不在View D的界限内,但它在View E的界限内。

View E是视图层级中包含触摸的最低的视图,因此它成为命中测试视图(hit-test view)。

//模拟代码:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) {
        return nil;
    }
    
    BOOL inside = [self pointInside:point withEvent:event];
    if (inside) {
        NSArray *subViews = self.subviews;
        // 对子视图从上向下找
        for (NSInteger i = subViews.count - 1; i >= 0; i--) {
            UIView *subView = subViews[i];
            CGPoint insidePoint = [self convertPoint:point toView:subView];
            UIView *hitView = [subView hitTest:insidePoint withEvent:event];
            if (hitView) {
                return hitView;
            }
        }
        return self;
    }
    return nil;
}
四、知识点应用
  1. 调用hitTest,获取到被点击的视图,也就是第一响应者:
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
    想让指定视图来响应事件,不再遍历子视图传递事件,可以通过重写hitTest方法。

  2. hitTest方法内部会通过调用pointInside,来判断点击区域是否在视图上,是则返回YES,不是则返回NO
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
    通过重写pointInside方法,可以将有效点击区域扩大。

  3. 另外,应用程序通过响应者来接收和处理事件(能够响应事件的对象都是UIResponder的子类对象,例如UIViewUIViewControllerUIApplication等)。当事件来到时,系统会将事件传递给合适的响应者,并且将其成为第一响应者。
    第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIRespondernextResponder决定,可以通过重写该属性来决定传递规则。当一个事件到来时,第一响应者没有接收消息,则顺着响应者链向后传递。

如果命中测试视图不能处理这个事件,就会往上传递
五、注意点

在遍历视图时,忽略以下三种情况的视图,如果视图具有以下特征则忽略:

  1. 视图的hidden等于YES
  2. 视图的alpha小于等于0.01
  3. 视图的userInteractionEnabledNO

但是视图的背景颜色是clearColor,并不在忽略范围内。

六、优先级

事件到来后先会执行hitTestpointInside操作,通过这两个方法找到第一响应者。当找到第一响应者并将其返回给UIApplication后,UIApplication会向第一响应者派发事件,并且遍历整个响应者链。开始会执行响应者链中的touches系列方法。会先执行touchesBegantouchesMoved方法,如果响应者链能够继续响应事件,则执行touchesEnded方法表示事件完成。如果响应者链中有能够处理当前事件的手势,则将事件交给手势处理,调用touchesCancelled方法将响应者链打断。
如果UIButton(所有继承自UIControl类)是第一响应者,则直接由UIApplication派发事件,不通过响应者链派发。如果其不能处理事件,则交给手势处理或响应者链传递。

  • 代码验证
  1. 自定义TestView重写touches系列方法:
@implementation TestView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesBegan TextView");
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesMoved TextView");
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesEnded TextView");
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesCancelled TextView");
}
@end
//点击结果
touchesBegan TextView
touchesEnded TextView

TestView或者其父控件添加UITapGestureRecognizer点击手势后:

//点击结果
touchesBegan TextView
tap
touchesCancelled TextView

view添加单击手势之后,原来的touchesEnded方法就无效了,继而执行touchesCancelled

  1. 自定义TestButton重写Tracking系列方法,并添加点击方法:
@implementation TestButton

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"beginTracking");
    return YES;
}

- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"continueTracking");
    return YES;
}

- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"endTracking");
}

- (void)cancelTrackingWithEvent:(UIEvent *)event {
    NSLog(@"cancelTracking");
}
//点击效果
beginTracking
endTracking
buttonToClick

给其父控件添加UITapGestureRecognizer点击手势后:

//点击效果
beginTracking
endTracking
buttonToClick
  • 优先级:系统的UIControl > 手势 > 自定义的UIControl

如果给TestButton添加UITapGestureRecognizer点击手势后:

//点击效果
beginTracking
tap
cancelTracking
  • 补充:最后响应的途径便是sendAction分发event到一个对象去处理:
按钮
手势
七、补充点
传递与响应
  • source1runloop用来处理mach port传来的系统事件的,source0是用来处理用户事件的。在之前Runloop执行流程中提到过source1source0

推荐参考:https://mp.weixin.qq.com/s/kkWWCb1Zy4d-lPRdPUoVHg
(文章部分来自此参考)

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

推荐阅读更多精彩内容