响应链传递底层代码猜测

1, 简单层面的touch事件的响应

在控制器的self.view中添加一个灰色的view,再将一个红色的view添加到灰色的view中:
代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    JKRLightGrayView *lightGrayView = [[JKRLightGrayView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    [self.view addSubview:lightGrayView];
    JKRRedView *redView = [[JKRRedView alloc] initWithFrame:CGRectMake(10, 10, 30, 30)];
    [lightGrayView addSubview:redView];
}

效果:


效果

重写灰色view、红色view、控制器的view的touchBegin方法:

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

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

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

现在点击红色view,log输出只有redView touch;点击浅灰色view,log输出只有lightGrayView touch;点击屏幕黑色区域(控制器的view的空白区域),log输出只有controller touch。目前为止,这就是简单touch事件的使用,获取view的点击事件并通过touch相关方法回调来进行相关操作。

分析一:

注释掉redView的touchesBegan方法,

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

然后重复分别点击每一个view,可以发现:点击红色view、灰色view时log输出都只有lightGrayView touch;点击控制器viewlog空白区域输出只有controller touch。
这里可以得出一个规律:默认状态下,如果子视图的view接收并重写了touch事件,那么只会在子视图中响应,父视图不会重复响应。

分析二:

将redView的尺寸调大,使它超过父视图lightGrayView:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor blackColor];
    
    JKRLightGrayView *lightGrayView = [[JKRLightGrayView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    [self.view addSubview:lightGrayView];
    JKRRedView *redView = [[JKRRedView alloc] initWithFrame:CGRectMake(10, 10, 120, 120)];
    [lightGrayView addSubview:redView];
}
QQ20170321-120740.png

这时点击超出的部分,发现输出的是:controller touch,即点击事件并没有被redView响应,而是被后面的控制器的view响应。
这里可以得到一个规律:默认状态下,子视图的响应范围不会超过它的父视图。

分析三:

将所有的touchesBegan方法中都加上

[super touchesBegan:touches withEvent:event];

重新点击redView输出如下:

redView touch  
lightGrayView touch
controller touch

这里可以得到一个规律:在touesBegan方法中调用super方法下,当前响应touch方法的视图的父视图也会响应touch方法。

2,深入判断响应对象的方法 hitTest调用规律测试

上面只是说了文档中描述顺序,下面听过判断响应者的相关方法,来验证之前描述的规律。
我们构建如上的视图:

视图结构

对每一个视图都做详细log输出,代码示例:

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    NSString *index = @"2";
    [index drawInRect:CGRectMake(rect.origin.x + 220, rect.origin.y, 20, 20) withAttributes:@{NSForegroundColorAttributeName:[UIColor blackColor], NSFontAttributeName:[UIFont systemFontOfSize:18]}];
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"lightGray view inside");
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"lightGray view is inside: %zd", isInside);
    return isInside;
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"lightGray view hit");
    UIView *view = [super hitTest:point withEvent:event];
    //UIView *view = [self jkr_hitTest:point withEvent:event];
    NSLog(@"lightGray view hit: %@", view);
    return view;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"lightGray view touchBegan");
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"lightGray view touchCancelled");
    [super touchesCancelled:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"lightGray view touchMoved");
    [super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"lightGray view touchEnded");
    [super touchesEnded:touches withEvent:event];
}

点击RedView输出如下:

yellow view hit
yellow view inside
yellow view is inside: 0
yellow view hit: (null)
lightGray view hit
lightGray view inside
lightGray view is inside: 1
green view hit
green view inside
green view is inside: 0
green view hit: (null)
red view hit
red view inside
red view is inside: 1
red view hit: <RedView: 0x7fb058c07ea0; frame = (0 0; 196 100); autoresize = RM+BM; layer = <CALayer: 0x60000003e940>>
lightGray view hit: <RedView: 0x7fb058c07ea0; frame = (0 0; 196 100); autoresize = RM+BM; layer = <CALayer: 0x60000003e940>>
red view touchBegan
lightGray view touchBegan
rootView touchBegan
red view touchEnded
lightGray view touchEnded
rootView view touchEnded

分析:通过log输出顺序可以发现,父视图hitTest方法首先调用,然后调用父视图pointInside方法,如果父视图有子视图,子视图继续吊用hitTest方法。从这里我们可以明显发现这个调用顺序是一个递归调用。

3,hitTest透析,重写hitTest方法完成响应者遍历

通过hitTest的调用顺序,模拟重写一个hitTest方法:

#import "UIView+Hittest.h"

@implementation UIView (Hittest)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"自定义 hittest: %@", self);
    if (self.alpha <= 0.01 || self.hidden || self.userInteractionEnabled == false) {
        return nil;
    }
    UIView *lastResultView = nil;
    if ([self pointInside:point withEvent:event]) {
        lastResultView = self;
        for (NSInteger i = self.subviews.count - 1; self.subviews.count && i >= 0; i--) {
            UIView *view = self.subviews[i];
            CGPoint convertPoint = [self convertPoint:point toView:view];
            UIView *currentResultView = [view hitTest:convertPoint withEvent:event];
            if (currentResultView) {
                lastResultView = currentResultView;
                break;
            }
        }
    }
    return lastResultView;
}

@end

重新点击,发现touch事件结果和系统一样。

系统默认下,视图可以响应触摸事件的条件

1,透明度不低于0.01
2,hidden为NO
3,userInteractionEnabled为YES

hitTest和pointInside方法的关系:

一个视图的hitTest方法首先调用,hitTest方法在判断视图满足可以成为响应者的基本条件(透明度、是否隐藏,是否响应触摸事件)后,调用它的pointInside方法。如果pointInside方法返回为true(即touch的点在该视图的响应范围内),那么就继续判断该视图是否有子视图,如果有子视图,则调用子视图的hitTest方法。直到所有子视图的hitTest方法调用完毕,最后将结果递归调用回父视图。所有视图的hitTest方法返回值为递归的最终结果。

hitTest方法的修改和pointInside方法的修改。

1,系统遍历响应者时,是否遍历一个视图的子视图前提是该视图pointInside方法的调用的结果。如果直接修改这视图的hitTest方法的返回值,确没有调用该视图的super方法和pointInside方法,那么这个视图的子视图就不会被遍历到,该视图会拦截所有子视图的touch事件响应。
2,同样,由于遍历递归的返回结果是由子视图传递到父视图,如果修改了父视图的hitTest方法,如果点击了子视图,那么该父视图的hitTest返回值会替换掉子视图的返回值,导致最终hitTest返回结果是父视图返回的值。
3,综上,直接修改hitTest的值,在响应者遍历的时候,会一定程度的打乱递归返回结果的逻辑,是不太好的行为。如果要修改一个视图的响应范围或者屏蔽touch事件,最好是修改pointInside的值。

4,重写hitTest方法可以应用的地方

1,让透明度低于0.01的视图响应触摸事件
2,让hidden的视图响应触摸事件
3,让userInteractionEnabled为NO的视图响应触摸事件
4,重定义响应触摸事件的视图应满足的条件

5,pointInside方法的修改和应用

测试一:

在RedView、LightGrayView、GreenView、YellowView的pointInside方法中都添加打印point参数的代码:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"yellow point: %@", NSStringFromCGPoint(point));
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"yellow view is inside: %zd", isInside);
    return isInside;
}

点击RedView查看输出如下:

yellow point: {23.666656494140625, -136}
yellow view is inside: 0
yellow view hit: (null)
lightGray point: {23.666656494140625, 20}
lightGray view is inside: 1
green point: {-114.33334350585938, -37}
green view is inside: 0
green view hit: (null)
red point: {23.666656494140625, 20}
red view is inside: 1
red view hit: <RedView: 0x7fdb23003e90; frame = (0 0; 196 100); autoresize = RM+BM; layer = <CALayer: 0x608000035080>>
lightGray view hit: <RedView: 0x7fdb23003e90; frame = (0 0; 196 100); autoresize = RM+BM; layer = <CALayer: 0x608000035080>>
red view touchBegan
lightGray view touchBegan
rootView touchBegan

过滤我们关注的point坐标输出如下:

yellow point: {23.666656494140625, -136}
lightGray point: {23.666656494140625, 20}
green point: {-114.33334350585938, -37}
red point: {23.666656494140625, 20}

通过输出可以得到结论,pointInside方法中的point的坐标并不是以屏幕为参照系,而是以当前视图为参照系。

测试二:

将yellowView的pointInside返回参数修改为true,点击LightGrayView和它的子视图:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"yellow point: %@", NSStringFromCGPoint(point));
    BOOL isInside = [super pointInside:point withEvent:event];
    isInside = YES;
    NSLog(@"yellow view is inside: %zd", isInside);
    return isInside;
}

查看输出如下:

yellow point: {112.66665649414062, -119.66667175292969}
yellow view is inside: 1
yellow view hit: <YellowView: 0x7fe5e8d14a00; frame = (33 303; 240 128); autoresize = RM+BM; layer = <CALayer: 0x608000028ae0>>
yellow view touchBegan
rootView touchBegan

可以看到,yellow会截取和它平级的在它之前添加的视图的点击事件。

应用一:缩减一个视图的响应范围

我们之前看到,红色视图有一部分被绿色遮盖


缩减一个视图的响应范围

当点击被遮盖部分的时候,是绿色视图响应点击事件。现在可以通过重写绿色视图的pointInside方法缩减绿色视图的响应范围,让红色视图来响应点击事件。之所以要修改绿色视图的pointInside方法,是因为GreenView是在RedView之前遍历到的,它的pointInside返回true后,会直接拦截和它平级的RedView的遍历。
修改代码如下:

//GreenView.m
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"green point: %@", NSStringFromCGPoint(point)); 
    if (point.x < 58 && point.y < 37) {
        return NO;
    } else {
        BOOL isInside = [super pointInside:point withEvent:event];
        NSLog(@"green view is inside: %zd", isInside);
        return isInside;        
    }
}

点击GreenView和RedView交叉的区域输出如下:

yellow point: {175.66665649414062, -78}
yellow view is inside: 0
yellow view hit: (null)
lightGray point: {175.66665649414062, 78}
lightGray view is inside: 1
green point: {37.666656494140625, 21}
green view hit: (null)
red point: {175.66665649414062, 78}
red view is inside: 1
red view hit: <RedView: 0x7f9041409110; frame = (0 0; 196 100); autoresize = RM+BM; layer = <CALayer: 0x60000003fb20>>
lightGray view hit: <RedView: 0x7f9041409110; frame = (0 0; 196 100); autoresize = RM+BM; layer = <CALayer: 0x60000003fb20>>
red view touchBegan
lightGray view touchBegan
rootView touchBegan

点击GreenView没有覆盖RedView的区域输出如下:

yellow point: {227.33332824707031, -44}
yellow view is inside: 0
yellow view hit: (null)
lightGray point: {227.33332824707031, 112}
lightGray view is inside: 1
green point: {89.333328247070312, 55}
green view is inside: 1
green view hit: <GreenView: 0x7f90414096e0; frame = (138 57; 94 63); autoresize = RM+BM; layer = <CALayer: 0x60000003fb80>>
lightGray view hit: <GreenView: 0x7f90414096e0; frame = (138 57; 94 63); autoresize = RM+BM; layer = <CALayer: 0x60000003fb80>>
green view touchBegan
lightGray view touchBegan
rootView touchBegan

可以看到,成功的缩减了GreenView接收touch事件的范围。

应用二:扩大一个视图的响应范围

还是看这个视图

扩大一个视图的响应范围

下面我们有一个需求,RedView是本来就有的视图,已经写好了touch事件的处理,现在在RedView上面添加了一个GreenView,即要让GreenView不遮盖RedView的点击事件,还要让点击GreenView的时候,也让RedView的touch事件来处理这个点击操作。

分析:

这里就需要做两部操作,首先,由于GreenView实在RedView之后添加的,所以当系统遍历响应者的时候GreenView如果满足响应条件会拦截和它平级的RedView的遍历。所以只扩大RedView的响应范围是不够的,需要先重写GreenView的pointInside方法让它不拦截:

//GreenView.m
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"green point: %@", NSStringFromCGPoint(point));
    return NO;
}

然后在扩大RedView的范围:

//RedView.m
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"red point: %@", NSStringFromCGPoint(point));
    CGRect extendFrame = CGRectMake(138, 57, 94, 63);
    BOOL isInside = [super pointInside:point withEvent:event] ||  CGRectContainsPoint(extendFrame, point);
    NSLog(@"red view is inside: %zd", isInside);
    return isInside;
}

现在无论点击RedView还是GreenView,都由RedView的touch事件来响应。

获取授权

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

推荐阅读更多精彩内容