一、响应链相关的两个核心函数
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 |