聊聊响应者链条

前言

今天面试滴滴的时候聊到了自己以前遇到的一个坑,其中涉及了一些响应者链条的事。本身这个bug就很有代表性(代表了自己对这块非常的不熟悉😃)。无独有偶,自己的好基友前几天腾讯电面一面时也遇到了这个问题。回来好好看了大神的博客。就顺手写个学习笔记自我总结下吧。

先聊聊响应链

UIResponder是所有可以响应事件的类的基类(从名字应该就可以看出来了),其中包括最常见的UIView和UIViewController甚至是UIApplication,所以我们的UIViewUIViewController都是作为响应事件的载体。

Apple🍎爸爸是这么说的:

The UIResponder class defines an interface for objects that respond to and handle events. It is the superclass of UIApplication, UIView and its subclasses (which include UIWindow). Instances of these classes are sometimes referred to as responder objects or, simply, responders.

先看看这个UIResponder的头文件:

#import <Foundation/Foundation.h>
#import <UIKit/UIKitDefines.h>
#import <UIKit/UIEvent.h>

NS_ASSUME_NONNULL_BEGIN

@class UIPress;
@class UIPressesEvent;

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject

- (nullable UIResponder*)nextResponder;

- (BOOL)canBecomeFirstResponder;    // default is NO
- (BOOL)becomeFirstResponder;

- (BOOL)canResignFirstResponder;    // default is YES
- (BOOL)resignFirstResponder;

- (BOOL)isFirstResponder;
- (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:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet * _Nonnull)touches NS_AVAILABLE_IOS(9_1);

注:这里我删除了一些此文无关的定义

那么UIResponder和我们讲的相应者链条到底是什么关系呢?其实在iOS系统中,所谓响应者链条就是由这些UIResponder链接起来的。你可以想象成链表,链接他们的就是上面定义的属性nextResponder链接起来的。

响应者链条

Hit-Testing View

简单了解响应者链条是什么后,就要今天的主角登场了:Hit-Testing View

上节我们讲到了响应者链条,但是并没有说清楚它的工作流程。比如上图中的intial view是怎么寻找到的?系统正是通过一个叫做Hit-Test过程找到这个initial obje的。

Hit-Test的目的就是寻找目前手指点击到的那个最前的view,也可以理解为responder
这个过程对应的方法在UIView里:

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds

当用户点击了手机屏幕时,UIApplication就会调用UIWindowhitTest: withEvent:方法。这个方法最终返回一个UiView。也就是我们要找到的那个最前的view。那这个方法具体流程是怎么样的呢?

我们拿下图说明下:


层次结构

UIWindow有一个MianVIew,MainView里面有三个subView:view A、view B、view C,他们各自有两个subView,他们层级关系是:view A在最下面,view B中间,view C最上(也就是addSubview的顺序,越晚add进去越在上面)。

现在假设手指点击在了绿色的View B上:
此时,UIApplication会调用UIWindowhitTest:WithEvent:方法,接着UIWindow调用上面的第二个方法:pointInside:withEvent:。现在手指点击的位置显然在MainViewbounds内,于是这个方法返回yes。紧接着,MainView开始遍历subviews。此处需要注意的是,这个方法会从index值较大的位置开始遍历,比如此处就会先找到VIew C上。因为它是最后一个被加入subviews数组里的。在调用了pointInside:withEvent:方法后,返回了NO。于是继续在MainViewsubview.index - 1的位置继续调用pointInside:withEvent:方法。如此循环,最终找到手指此时触摸的位置:View B.1

触摸发生时

完整流程:


完整流程

我们可以看到:判断当前这个view是不是hitView时,需要同时满足以下四个条件:

view.userInteractionEnable == YES
view.hidden == NO
view.alpha > 0.01f
[view pointInside:point withEvent:event] == YES

代码实现起来还是比较简单的😄:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (self.hidden || self.alpha <= 0.01f || [self pointInside:point withEvent:event] || !self.userInteractionEnabled) {
    return nil;
}
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {//注意倒叙

    CGPoint newPoint = [subview convertPoint:point fromView:self];
    UIView *hitView = [subview hitTest:newPoint withEvent:event];

    if (hitView) {
        return hitView;
    }
}
return self;

}

Hit-Testing View应用

最常用的:扩大按钮热区

项目中经常有扩大某个按钮热区的需求,相信实际做过项目的童鞋都有过这样的经历。以前小弟的做法是把按钮宽高调大一点,但是这样也会导致按钮图片位移,还要改按钮的UIEdgeInsets等属性,很是蛋疼。有了Hit-Testing View,我们就可以这样写:😄


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGRect originRect = self.bounds;
    originRect.origin = CGPointMake(-15.0f, -10.0f);
    originRect.size = CGSizeMake(originRect.size.width + 30.0f, originRect.size.height + 10.0f + 10.0f);
    
    CGPoint touchPoint = [self convertPoint:point toView:self];
    if (CGRectContainsPoint(originRect, touchPoint)) {
        return self;
    }
    return [super hitTest:point withEvent:event];
}

事件的传递

有了响应者链条,事件的传递也就水到渠成了。在UIApplication调用了UIWIndowhitTest:withEevent:方法并反悔了一个hitView后,就会通过sendEvent:这个方法将事件传递给当前的hitView。如果当前的hitView处理不了该事件,就会将事件交由自己的nextResponder处理,如此递归。若最后交由UIApplication仍然处理不了该事件,系统就会抛弃该事件。

摘抄一段大神的博客:

如果view重写了touch方法,我们一般会看到的效果是,这个view响应了事件之后,它nextResponder不会收到这个事件,即使重写了nextRespondertouch方法。这个时候如果想事件继续传递下去,可以调用[super touchesBegan:touches withEvent:event],不建议直接调用[self.nextRespondertouchesBegan:touches withEvent:event]

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

推荐阅读更多精彩内容