iOS开发——响应链那些事

一、响应链相关的两个核心函数

a、返回当前可以响应的此次操作的视图

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

b、返回当前碰触的屏幕坐标是否在当前视图中

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

二、响应链逻辑实验相关代码

自定义Button1、Button2、Button3 继承自UIButton,重写hitTest、pointInside两个函数增加log

以Button1举例:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    NSLog(@"-----hitTest star Button1-----");
    UIView* view = [super hitTest:point withEvent:event];
    NSLog(@"-----hitTest end Button1----- %@",view);
    return view;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    BOOL result = [super pointInside:point withEvent:event];
    NSLog(@"-----pointInside Button1----- %@",@(result));
    return result;
}

实验一、将三个button实例化,平行添加到VC视图中,互相不重叠

    Button1 *btn1 = [[Button1 alloc]initWithFrame:CGRectMake(100, 100, 50, 50)];
    [btn1 setTitle:@"btn1" forState:UIControlStateNormal];
    btn1.backgroundColor = [UIColor redColor];
    
    Button2 *btn2 = [[Button2 alloc]initWithFrame:CGRectMake(160, 100, 50, 50)];
    [btn2 setTitle:@"btn2" forState:UIControlStateNormal];
    btn2.backgroundColor = [UIColor yellowColor];
    
    Button3 *btn3 = [[Button3 alloc]initWithFrame:CGRectMake(220, 100, 50, 50)];
    [btn3 setTitle:@"btn3" forState:UIControlStateNormal];
    btn3.backgroundColor = [UIColor blueColor];
    
    [self.view addSubview:btn1];
    [self.view addSubview:btn2];
    [self.view addSubview:btn3];

操作一:此时点击空白处,打印log如下:

2020-04-05 14:35:28.522130+0800 hitTestDemo[1815:64004] -----hitTest star Button3-----
2020-04-05 14:35:28.522224+0800 hitTestDemo[1815:64004] -----pointInside Button3----- 0
2020-04-05 14:35:28.522281+0800 hitTestDemo[1815:64004] -----hitTest end Button3----- (null)
2020-04-05 14:35:28.522333+0800 hitTestDemo[1815:64004] -----hitTest star Button2-----
2020-04-05 14:35:28.522382+0800 hitTestDemo[1815:64004] -----pointInside Button2----- 0
2020-04-05 14:35:28.522449+0800 hitTestDemo[1815:64004] -----hitTest end Button2----- (null)
2020-04-05 14:35:28.522512+0800 hitTestDemo[1815:64004] -----hitTest star Button1-----
2020-04-05 14:35:28.522613+0800 hitTestDemo[1815:64004] -----pointInside Button1----- 0
2020-04-05 14:35:28.522662+0800 hitTestDemo[1815:64004] -----hitTest end Button1----- (null)

//执行第二遍
2020-04-05 14:35:28.522748+0800 hitTestDemo[1815:64004] -----hitTest star Button3-----
2020-04-05 14:35:28.522793+0800 hitTestDemo[1815:64004] -----pointInside Button3----- 0
2020-04-05 14:35:28.522877+0800 hitTestDemo[1815:64004] -----hitTest end Button3----- (null)
2020-04-05 14:35:28.523017+0800 hitTestDemo[1815:64004] -----hitTest star Button2-----
2020-04-05 14:35:28.542243+0800 hitTestDemo[1815:64004] -----pointInside Button2----- 0
2020-04-05 14:35:28.542305+0800 hitTestDemo[1815:64004] -----hitTest end Button2----- (null)
2020-04-05 14:35:28.542364+0800 hitTestDemo[1815:64004] -----hitTest star Button1-----
2020-04-05 14:35:28.542424+0800 hitTestDemo[1815:64004] -----pointInside Button1----- 0
2020-04-05 14:35:28.542466+0800 hitTestDemo[1815:64004] -----hitTest end Button1----- (null)

根据log可见,主视图view的hitTest中会遍历所有子视图,并执行子视图的hitTest方法,遍历顺序是subviews数组的倒序。

点击空白视图的时候,Button1、Button2、Button3 分别执行中hitTest,在hitTest中调用pointInside,均返回了0,说明当前碰触的屏幕坐标均不在三个视图中;hitTest返回的值都是null,说明当前操作,这三个视图均无法提供可以响应此次操作的视图。

操作二:此时点击红色按钮Button1,打印log如下:

2020-04-05 14:46:34.098658+0800 hitTestDemo[1899:72303] -----hitTest star Button3-----
2020-04-05 14:46:34.098857+0800 hitTestDemo[1899:72303] -----pointInside Button3----- 0
2020-04-05 14:46:34.098970+0800 hitTestDemo[1899:72303] -----hitTest end Button3----- (null)
2020-04-05 14:46:34.099065+0800 hitTestDemo[1899:72303] -----hitTest star Button2-----
2020-04-05 14:46:34.099160+0800 hitTestDemo[1899:72303] -----pointInside Button2----- 0
2020-04-05 14:46:34.099247+0800 hitTestDemo[1899:72303] -----hitTest end Button2----- (null)
2020-04-05 14:46:34.099315+0800 hitTestDemo[1899:72303] -----hitTest star Button1-----
2020-04-05 14:46:34.099393+0800 hitTestDemo[1899:72303] -----pointInside Button1----- 1
2020-04-05 14:46:34.099888+0800 hitTestDemo[1899:72303] -----hitTest end Button1----- >

//执行第二遍
2020-04-05 14:46:34.100059+0800 hitTestDemo[1899:72303] -----hitTest star Button3-----
2020-04-05 14:46:34.100147+0800 hitTestDemo[1899:72303] -----pointInside Button3----- 0
2020-04-05 14:46:34.100229+0800 hitTestDemo[1899:72303] -----hitTest end Button3----- (null)
2020-04-05 14:46:34.100310+0800 hitTestDemo[1899:72303] -----hitTest star Button2-----
2020-04-05 14:46:34.100397+0800 hitTestDemo[1899:72303] -----pointInside Button2----- 0
2020-04-05 14:46:34.100467+0800 hitTestDemo[1899:72303] -----hitTest end Button2----- (null)
2020-04-05 14:46:34.101958+0800 hitTestDemo[1899:72303] -----hitTest star Button1-----
2020-04-05 14:46:34.102017+0800 hitTestDemo[1899:72303] -----pointInside Button1----- 1
2020-04-05 14:46:34.102108+0800 hitTestDemo[1899:72303] -----hitTest end Button1----- >

点击Button1视图的时候,仍然是从Button3进行检测,直到检测到Button1时,pointInside返回的值为1,说明当前碰触的屏幕坐标在Button1视图中;Button1的hitTest返回的值是Button1实例,说明当前操作可以被Button1实例响应

实验二、Button1添加到主视图上,Button2和Button3平行添加到Button1上,Button2和Button3是Button1的子视图

    Button1 *btn1 = [[Button1 alloc]initWithFrame:CGRectMake(100, 100, 200, 200)];
    [btn1 setTitle:@"btn1" forState:UIControlStateNormal];
    btn1.userInteractionEnabled = false;
    btn1.backgroundColor = [UIColor redColor];
    
    Button2 *btn2 = [[Button2 alloc]initWithFrame:CGRectMake(30, 30, 50, 50)];
    [btn2 setTitle:@"btn2" forState:UIControlStateNormal];
    btn2.backgroundColor = [UIColor yellowColor];
    
    Button3 *btn3 = [[Button3 alloc]initWithFrame:CGRectMake(110, 30, 50, 50)];
    [btn3 setTitle:@"btn3" forState:UIControlStateNormal];
    btn3.backgroundColor = [UIColor blueColor];
    
    [self.view addSubview:btn1];
    [btn1 addSubview:btn2];
    [btn1 addSubview:btn3];

操作一:此时点击空白处,打印log如下:

2020-04-05 14:55:41.633552+0800 hitTestDemo[1922:76463] -----hitTest star Button1-----
2020-04-05 14:55:41.633929+0800 hitTestDemo[1922:76463] -----hitTest end Button1----- (null)

//执行第二遍
2020-04-05 14:55:41.634152+0800 hitTestDemo[1922:76463] -----hitTest star Button1-----
2020-04-05 14:55:41.634229+0800 hitTestDemo[1922:76463] -----hitTest end Button1----- (null)

当前主视图的子视图只有Button1,Button1无法响应

操作二:此时点击Button1(不被Button2、Button3覆盖的红色区域),打印log如下:

2020-04-05 14:57:26.486880+0800 hitTestDemo[1941:78733] -----hitTest star Button1-----
2020-04-05 14:57:26.486976+0800 hitTestDemo[1941:78733] -----pointInside Button1----- 1
2020-04-05 14:57:26.487081+0800 hitTestDemo[1941:78733] -----hitTest star Button3-----
2020-04-05 14:57:26.487153+0800 hitTestDemo[1941:78733] -----pointInside Button3----- 0
2020-04-05 14:57:26.487211+0800 hitTestDemo[1941:78733] -----hitTest end Button3----- (null)
2020-04-05 14:57:26.487274+0800 hitTestDemo[1941:78733] -----hitTest star Button2-----
2020-04-05 14:57:26.487350+0800 hitTestDemo[1941:78733] -----pointInside Button2----- 0
2020-04-05 14:57:26.487401+0800 hitTestDemo[1941:78733] -----hitTest end Button2----- (null)
2020-04-05 14:57:26.487777+0800 hitTestDemo[1941:78733] -----hitTest end Button1----- >

//执行第二遍
2020-04-05 14:57:26.487885+0800 hitTestDemo[1941:78733] -----hitTest star Button1-----
2020-04-05 14:57:26.487955+0800 hitTestDemo[1941:78733] -----pointInside Button1----- 1
2020-04-05 14:57:26.488003+0800 hitTestDemo[1941:78733] -----hitTest star Button3-----
2020-04-05 14:57:26.503970+0800 hitTestDemo[1941:78733] -----pointInside Button3----- 0
2020-04-05 14:57:26.504106+0800 hitTestDemo[1941:78733] -----hitTest end Button3----- (null)
2020-04-05 14:57:26.504199+0800 hitTestDemo[1941:78733] -----hitTest star Button2-----
2020-04-05 14:57:26.504263+0800 hitTestDemo[1941:78733] -----pointInside Button2----- 0
2020-04-05 14:57:26.504329+0800 hitTestDemo[1941:78733] -----hitTest end Button2----- (null)
2020-04-05 14:57:26.504457+0800 hitTestDemo[1941:78733] -----hitTest end Button1----- >

由log可见,在Button1的hitTest中先调pointInside,检测当前操作坐标是在Button1范围内,然后分别倒序遍历Button1的子视图[Button2,Button3],分别执行hitTest方法,在hitTest中调研pointInside,Button2,Button3的pointInside均返回0,hitTest均返回null,可见此次操作均无法在Button2,Button3中得到响应,最后Button1实例将自己提供为此次操作的相应View;

操作三:此时点击Button2,打印log如下:

2020-04-05 15:11:49.864895+0800 hitTestDemo[1941:78733] -----hitTest star Button1-----
2020-04-05 15:11:49.865002+0800 hitTestDemo[1941:78733] -----pointInside Button1----- 1
2020-04-05 15:11:49.865078+0800 hitTestDemo[1941:78733] -----hitTest star Button3-----
2020-04-05 15:11:49.865134+0800 hitTestDemo[1941:78733] -----pointInside Button3----- 0
2020-04-05 15:11:49.865193+0800 hitTestDemo[1941:78733] -----hitTest end Button3----- (null)
2020-04-05 15:11:49.865240+0800 hitTestDemo[1941:78733] -----hitTest star Button2-----
2020-04-05 15:11:49.865307+0800 hitTestDemo[1941:78733] -----pointInside Button2----- 1
2020-04-05 15:11:49.865489+0800 hitTestDemo[1941:78733] -----hitTest end Button2----- >
2020-04-05 15:11:49.865579+0800 hitTestDemo[1941:78733] -----hitTest end Button1----- >

//执行第二遍
2020-04-05 15:11:49.865675+0800 hitTestDemo[1941:78733] -----hitTest star Button1-----
2020-04-05 15:11:49.881797+0800 hitTestDemo[1941:78733] -----pointInside Button1----- 1
2020-04-05 15:11:49.881889+0800 hitTestDemo[1941:78733] -----hitTest star Button3-----
2020-04-05 15:11:49.881958+0800 hitTestDemo[1941:78733] -----pointInside Button3----- 0
2020-04-05 15:11:49.882005+0800 hitTestDemo[1941:78733] -----hitTest end Button3----- (null)
2020-04-05 15:11:49.882046+0800 hitTestDemo[1941:78733] -----hitTest star Button2-----
2020-04-05 15:11:49.882114+0800 hitTestDemo[1941:78733] -----pointInside Button2----- 1
2020-04-05 15:11:49.882201+0800 hitTestDemo[1941:78733] -----hitTest end Button2----- >
2020-04-05 15:11:49.882284+0800 hitTestDemo[1941:78733] -----hitTest end Button1----- >

由log可见,仍然是,在Button1的hitTest中先调pointInside,检测当前操作坐标是在Button1范围内,然后分别倒序遍历Button1的子视图[Button2,Button3],分别执行hitTest方法,在hitTest中调研pointInside,Button3的pointInside均返回0,hitTest返回null,Button2的pointInside均返回1,hitTest返回Button2实例,可见此次操作Button2实例将自己提供为此次操作的相应View;

附加实验:将Button2的userInteractionEnabled 设置为NO,重复操作三,log如下:

2020-04-05 15:18:31.830530+0800 hitTestDemo[1993:88543] -----hitTest star Button1-----
2020-04-05 15:18:31.830673+0800 hitTestDemo[1993:88543] -----pointInside Button1----- 1
2020-04-05 15:18:31.830767+0800 hitTestDemo[1993:88543] -----hitTest star Button3-----
2020-04-05 15:18:31.830835+0800 hitTestDemo[1993:88543] -----pointInside Button3----- 0
2020-04-05 15:18:31.830923+0800 hitTestDemo[1993:88543] -----hitTest end Button3----- (null)
2020-04-05 15:18:31.831013+0800 hitTestDemo[1993:88543] -----hitTest star Button2-----
2020-04-05 15:18:31.831106+0800 hitTestDemo[1993:88543] -----hitTest end Button2----- (null)
2020-04-05 15:18:31.831710+0800 hitTestDemo[1993:88543] -----hitTest end Button1----- >

//执行第二遍
2020-04-05 15:18:31.831881+0800 hitTestDemo[1993:88543] -----hitTest star Button1-----
2020-04-05 15:18:31.831960+0800 hitTestDemo[1993:88543] -----pointInside Button1----- 1
2020-04-05 15:18:31.832045+0800 hitTestDemo[1993:88543] -----hitTest star Button3-----
2020-04-05 15:18:31.832130+0800 hitTestDemo[1993:88543] -----pointInside Button3----- 0
2020-04-05 15:18:31.832204+0800 hitTestDemo[1993:88543] -----hitTest end Button3----- (null)
2020-04-05 15:18:31.832261+0800 hitTestDemo[1993:88543] -----hitTest star Button2-----
2020-04-05 15:18:31.833534+0800 hitTestDemo[1993:88543] -----hitTest end Button2----- (null)
2020-04-05 15:18:31.833620+0800 hitTestDemo[1993:88543] -----hitTest end Button1----- >

log与操作三类似,唯一不一致的是最后Button2的pointInside均返回1,hitTest返回null,Button1的hitTest返回Button1自身实例,可见userInteractionEnabled的View无法被当做响应者的view提供,据测试(透明度小于0.01或者被隐藏的view)也无法被当做响应者的view提供。

三、响应链逻辑总结

根据实验逻辑,模拟下 hitTest 方法的大概实现:

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 如果交互未打开,或者透明度小于0.01 或者 视图被隐藏
    if (!self.userInteractionEnabled || self.alpha < 0.01 || self.isHidden) {
        return nil;
    }
    // 如果 touch 的point 在 self 的bounds 内
    if ([self pointInside:point withEvent:event]) {
        for ( int i = (int)self.subviews.count - 1; i >=0; i--) {
            //倒序遍历
            UIView* subView = self.subviews[i];
            //进行坐标转化
            CGPoint coverPoint = [subView convertPoint:point fromView:self];
            // 调用子视图的 hitTest 重复上面的步骤。找到了,返回hitTest view ,没找到返回有自身处理
            UIView *hitTestView = [subView hitTest:coverPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

四、userInteractionEnabled原理及及应用

userInteractionEnabled是视图的一个属性,标识该视图是否可以响应用户操作。当userInteractionEnabled为NO,则该视图永远无法成为操作的响应者,hitTest函数的返回值永远是null。只有userInteractionEnabled为YES,该视图才有成为响应者的机会。

在IOS中UI控件的userInteractionEnabled默认值并不相同:

UI控件 userInteractionEnabled默认值
UIImageView NO
UILabel NO
UIView YES

你可能感兴趣的:(iOS开发——响应链那些事)