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];
}
这时点击超出的部分,发现输出的是: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事件来响应。