iOS中的事件和响应者链

I. 一个物理触摸事件的完整流程:

  • 用户触摸 iPhone 的触摸屏,硬件感应到会通知操作系统
  • 操作系统根据硬件提供的信息打包成UITouch 中的 UIEvent对象
  • 将事件传递给当前运行程序的事件队列
  • 这个事件会被 runloop 发送给被触摸视图所在的 window
  • window 会启动 hit-testing, 找到 hit-test view
  • 如果这个 view 不能处理这个触摸事件,事件会沿着响应者链找到处理它的对象,或者被系统丢弃

II. 基本概念:

1. Event Type:

iOS 有三种事件类型:

  • 触控事件 (UIeventTypeTouches): 单点/多点触控以及手势操作
  • 传感器事件 (UIEventTypeMotion): 各种传感器等
  • 远程控制事件 (UIEventTypeRemoteControl):

2. 响应者对象(Responder Object)

响应者对象,是可以相应和处理事件的对象
一个事件可以由多个 responder 接收,第一个接收事件的对象,就叫 first responder
iOS 中,UIResponder 就是代表响应者的类,几乎大部分的类都是它的子类:UIWindow,UIView,UIControl,UIController 等

III. 响应者链:

这段摘自苹果官方文档,前两个不想翻译╮(╯_╰)╭,就原文摘下来了

1. 第一响应者接受事件(the first responder receives some event first)

It receives key events, motion events, and action messages, among others. (Mouse events and multitouch events first go to the view that is under the mouse pointer or finger; that view might or might not be the first responder.)

2. 响应者链启用协作事件处理

  • If the first responder cannot handle an event or action message, it forwards it to the “next responder” in a linked series called the responder chain.
  • If an object in the responder chain cannot handle the event or action, it passes the message to the next responder in the chain.
  • The message travels up the chain, toward higher-level objects, until it is handled. If it isn't handled, the app discards it.
    image

3. 事件的路径(The Path of an event)

通常情况下,事件在一条响应链的路径的开端是一个视图-第一响应者或者触摸点或者鼠标点击所在的视图范围.从这里开始,一直沿着视图层级到达 window,直到全局的 APP 对象,然而 iOS 里的响应者链有一些变化:

  • 如果一个视图是被一个 VC 管理,并且这个视图不能处理这个事件,那么管理它的 VC 将会成为下一个响应者.
  • VC 如果有 Super VC,那么它的下个响应者是其 Super VC 的最表层的 View,如果没有,那么 nextResponder 就会是 UIWindow
  • UIApplication 是响应者链的重点,它的 nextResponder 是 nil.

需要说明的是,如果当前 responder 不处理事件,并且想将其传递给 nextResponder,需要手动写代码,才会继续往下传递,否则会被废弃.

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{  
    // 将事件传递给 nextResponder
    id theNextResponder = [self nextResponder];
    [theNextResponder touchesBegan:touches withEvent:event];
}

IV. Hit-Test View 与 Hit-Testing

1. 是什么?

Hit-Test View 就是事件被触发时和用户交互的对象,寻找它的过程就叫 Hit-Testing
一开始 Window 会调用hitTest:withEvent: 方法,它的所有子视图都会递归调用这个方法,知道找到需要的 View. 调用结束后,这个 view,和 view 上面依附的手势都会和 UITouch 对象关联起来,这个 UITouch 会作为后面的事件传递的参数

一种可能的 hitTest:withEvent: 方法的内部实现:
整个过程可以总结为“官兵来府里搜查”的小故事

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    // 先拿探测仪检测下东西是不是在自己家大院儿的领地范围(古代也可以有古代的检测仪呀)
    if ([self pointInside:point withEvent:event]) {
        // 检测到在院子里
        //把儿子们叫来,一个个审问,而且先问年龄大的(倒序)
        for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
            // 每个儿子性格不同,要转换不同的说话方式(坐标转换),才肯说实话
            CGPoint convertedPoint = [subView convertPoint:point fromView:self];
            // 儿子家里可能还有儿子,回去问清楚再来给爸爸汇报(调用儿子的hitTest:withEvent:方法)
            UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
            // 破案了
            if (hitTestView) {
                // 大义灭亲交出儿子
                return hitTestView;
            }
        }
        // 找儿子问了半天原来发现东西在自己屋里
        return self;
    }
    // 官爷您要找的东西,不在我这里~
    return nil;
}

2. 有什么用?

a. 扩大视图点击区域
b. 将事件传递给下层的视图
c. 将事件传递给子视图

a. 扩大视图点击区域

比如你可能碰见过一个场景:

测试/产品/UI 拿着手机过来找你说:
"xxx 你这个按钮怎么这么小,点都点不住啊?"
你无辜的说:
"UI 给我的尺寸就这么大,并且切的图四周没有留白,我也只能给这个 button 做这么大."

身为一个为用户体验着想的开发者,怎么能这么不顾用户的使用体验呢?

所以你可以这么干:

增加某个 view 的点击区域:

// 在自定义视图的 m 文件里实现以下方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    CGRect touchRect = CGRectInset(self.bounds, -10, -10);
    if (CGRectContainsPoint(touchRect, point)) {
        for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subView convertPoint:point fromView:self];
            UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

b. 将事件传递给下层的兄弟视图(视觉上更下层,实际上是更先被加到父视图上的兄弟视图)

比如两个重叠的视图,不想让在上层的视图处理事件,可以有以下做法:

  1. B 视图的userInteractionEnabled = NO(B 在系统调用hit testing过程中返回了nil)
  2. 重写 B 的 hitTest:withEvent: 方法,并返回 nil (如果点击的是交叉区域,hit testing view 会是 A, 如果点击B 的其他区域, hit testing view可能是它们的父视图)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *hitTestView = [super hitTest:point withEvent:event];
    if (hitTestView == self) {
        return nil;
    }
    return hitTestView;
}

c. 将事件传递给子视图

栗子如下,摘自这篇博客

scrollview

UIView 有一个子视图: scrollview(蓝色部分),没有占据整个 View 的区域,scrollview 里面是可以滑动的 imageView,实例是做了一个旋转木马的效果,然而你会发现,如果你拖动蓝色区域外,图片是不会滑动的.

想要顺利的让整个视图都能响应滑动手势,只需要在外层的 UIView 中实现 hitTest:withEvent: 方法,并且把 scrollView return 出去当做hit test view.

V. 事件处理

前面说到了,"如果一个 responder 不处理事件...",事件是怎么处理的呢?
必须重写4种方法:

  • touchesBegin:withEvent
  • touchesMoved:withEvent
  • touchesEnded:withEvent
  • touchedCancelled:withEvent (例如被来电打断的事件)

但 UIKit 的子类只需要实现与你感兴趣的方法,但必须调用super的实现

参考:

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

推荐阅读更多精彩内容

  • 重点参考链接: View Programming Guide for iOS https://developer....
    Kevin_Junbaozi阅读 4,394评论 0 15
  • 在iOS开发中经常会涉及到触摸事件。本想自己总结一下,但是遇到了这篇文章,感觉总结的已经很到位,特此转载。作者:L...
    WQ_UESTC阅读 5,987评论 4 26
  • 用户以多种方式操纵他们的iOS设备,例如触摸屏幕或摇动设备。 iOS会解释用户何时以及如何操作硬件并将此信息传递到...
    坤坤同学阅读 3,975评论 7 19
  • 本文主要讲解iOS触摸事件的一系列机制,涉及的问题大致包括: 触摸事件由触屏生成后如何传递到当前应用? 应用接收触...
    baihualinxin阅读 1,192评论 0 9
  • 一. Hit-Testing 什么是Hit-Testing?对于触摸事件, window首先会尝试将事件交给事件触...
    面糊阅读 814评论 0 50