iOS事件传递&视图响应链

1.事件传递的流程:
事件传递图.png
2.事件传递图示
事件传递详解.png

如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。
例如,不想让蓝色的view接收事件,那么可以设置蓝色的view的userInteractionEnabled = NO;那么点击黄色的view或者蓝色的view所产生的事件,最终会由橙色的view处理,橙色的view就会成为最合适的view。
所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理!也就是说,如果蓝色视图不能处理事件,点击蓝色视图产生的触摸事件不会由被点击的视图(蓝色视图)处理!
注意:如果设置父控件的透明度或者hidden,会直接影响到子控件的透明度和hidden。如果父控件的透明度为0或者hidden = YES,那么子控件也是不可见的!

3.流程描述:
  • 我们点击屏幕产生触摸事件,系统会将这个事件加入到一个由UIApplication管理的事件队列中,UIApplication会从消息队列里取事件分发下去,首先传给UIWindow
  • 在UIWindow中就会调用 hitTest:withEvent: 方法去返回一个最终响应的视图
  • 在hitTest:withEvent方法中就会去调用第二个方法 pointInside:withEvent: 去判断当前点击的point是否在UIWindow范围内,如果是的话,就会去遍历它的子视图来查找最终响应的子视图
  • 同级view的遍历方式是使用倒序的方式来遍历子视图,也就是说最后添加的子视图会最先遍历,在每一个视图中都去回去调用它的 hitTest:withEvent: 方法,可以理解成为是一个递归调用
  • 最终会返回一个响应视图,如果返回的视图有值,那么这个视图就作为最终响应视图,结束整个事件传递,如果没有值,那么就会将UIWindow作为响应值
4.hitTest:withEvent: 方法的流程
  • 首先会判断当前视图的hiden属性、是否可以交互及透明度是否大于0.01,如果满足条件则进入下一步,否则返回nil
  • 然后调用pointInside:withEvent:方法来判断这个点是否在当前范围内,如果满足条件进入下一步,否则返回nil
  • 返回以倒序的方式遍历它的子视图,在每一个子视图中取调用hitTest:withEvent:,如果有一个子视图返回了一个最终的响应视图,就将这个视图返回给调用方,结束流程。如果全部遍历完成都没有找到一个最终响应视图,因为点击位置在当前视图范围内,就将当前视图作为最终响应视图返回
二、视图响应链
2.1 事件的分类
  • multitouch events
  • motion events
  • remote control events
  • Multitouch Events: 所谓的多点触摸事件,非常好理解,即用户触摸屏幕交互产生的事件类型。
  • Motion Events: 所谓的移动事件。是指用户在摇晃,移动和倾斜手机的时候产生的事件成为移动事件。这类事件依赖于iPhone手机里面的加速计,陀螺仪等传感器。
  • Remote Control Events:所谓的远程控制事件。这个事件从名称上面看,不太好理解。但其实,这个事件指的是用户在操作多媒体的时候产生的事件。比如,播放音乐、视频等。
2.2 什么是Responder
  • Responder的属性和方法,从下面的方法可以看出UIResponder可以处理Touchevent,motionevent,remote control event
- (UIResponder )nextResponder;
- (BOOL)canBecomeFirstResponder;    // default is NO
- (BOOL)becomeFirstResponder;
// Touch Event
- (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;
// Motion Event
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent )event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent )event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent )event NS_AVAILABLE_IOS(3_0);
// Remote Control Event
(void)remoteControlReceivedWithEvent:(UIEvent)event NS_AVAILABLE_IOS(4_0);

注意有个很重要的方法,nextResponder,很明显可以看出来响应是一条链表结构,通过nestResponder找到下一个responder。这里是从第一个responder开始通过nextresponder传递事件,直到有responder响应了事件就停止传递;如果传到最后一个responder都没有被响应,那么该事件就被抛弃。
那么,谁是第一个resopnder呢? responder是怎么响应的呢?responder响应后为什么不往下传递了呢?稍后会一一回答

2.3 UIResponder的衍生类:

UIApplication UIViewController UIView都是继承UIResponder,都可以传递和响应事件


image.png

那么就可以这么理解,我们看到的一个界面,可能是由一个UIApplication和多个 UIViewController UIView组成,他们都是responder,他们一起组成了响应连。每次发生触摸事件,该事件就在这条响应链里传递

2.4 谁是第一个responder?

拿touchevent事件举例,一般情况下(因为有开放可以主动设置firstresponder),当前正在点击的视图对象就是first responder。

2.5 如何寻找first responder?
#事件传递的两个核心方法

#第一个方法返回一个UIView,是用来寻找哪一个视图来响应这个事件
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
#第二个方法是用来判断某一个点击的位置 是否在视图范围内,如果在就返回YES
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds

hitTest和pointinside是系统遍历寻找firstresponder的方法。最终返回的view就是当前触摸的first responder

2.6 hitTest遍历寻找first responder 的规则:

1)首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
2)若返回NO,则hitTest:withEvent:返回nil;
3)若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;那么,后面的addsubview进来的子view就会优先被选中为first responder
4)若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
注意:子view返回非空对象,若该子view还拥有自己的subviews,那么步骤3是个递归遍历。
5)若所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。

事件传递图解.png
  • ios没有源码,下面是模拟源码写的
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *result = [super hitTest:point withEvent:event];
    CGPoint buttonPoint = [underButton convertPoint:point fromView:self];
    if ([underButton pointInside:buttonPoint withEvent:event]) {
        return underButton;
    }
    return result;
}

从上面规则可以知道,视图超出父视图的区域是不会参与到遍历的,这是没有意义的计算。加上这个,一共有4种情况的view是不会参与到遍历的

1)隐藏(hidden=YES)的视图
2)禁止用户操作(userInteractionEnabled=NO)的视图
3)alpha<0.01的视图
4)视图超出父视图的区域

也就是说这四种情况的视图,以及他们的子视图是不会成为responder的。

  • 下面通过一个比较直观的图形来讲述上面的规则


    image.png

假设用户点击了视图D:

  1. 检测到点击坐标在View A范围之内。
  2. 继续检测点击范围是否在其子视图B,C范围内。发现点击范围在视图C范围内,则忽略掉B视图及其子视图分支。
  3. 继续检测点击范围是否在其子视图D范围内,如果是,则用户当前视图即为视图D。如果不是,继续检测其子视图。
  4. 总结:iOS系统会从父视图向子视图依次查找,直到找到点击范围在当前视图边界范围以内。如果点击范围在某子视图范围内,并且没有了子视图,则该视图即为当前点击视图。如果点击范围在某子视图范围之内,并且不在其子视图范围之内,则点击视图即为当前点击视图。
  • 总结:事件的传递和响应
    从上面可以看出,事件的传递方向是(hittest就是事件的传递):

UIApplication -> UIWindow ->ViewController-> UIView -> initial view

而Responder传递方向是(还记得nextResponder吗):
Initial View -> Parent View -> ViewController -> Window -> Application

如果最终传递到Application对象,依然没有对事件作出响应,事件就会被舍弃掉。

  • 怎么样才算是对事件做出了响应呢

在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[supertouches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法

  • 点击、摇动、滑动、旋转等会被系统封装成UIEvent,放到事件队列里等待UIApplication去取,然后寻找响应者,找到对应的方法并执行的过程就是响应。
  • 通过上述的传递事件会找到第一响应者,这时就用第一响应者来响应。
  • 如果第一响应者不响应,不响应的传递流程示图
    视图响应链.png

点击当前视图initial View - initial View的父视图view - view controller - UIWindow - UIApplication

  • 通过代码来展示
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%@ touch begin", self.class);
     UIResponder *next = [self nextResponder];
     while (next) {
         NSLog(@"下一个响应者%@",next.class);
         next = [next nextResponder];
     } 
}
  • 打印结果:


    image.png
  • 如果传递到UIApplication也没有响应,则这个事件作废.

转自简书:https://www.jianshu.com/p/94b0539b2178
转自博客:ios事件传递和响应机制

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