最近公司分享会上有同事分享了事件响应链的一些细节和逻辑,借这个机会把我觉得要注意的点整理一下。
1、事件传递顺序
事件的传递顺序,我就不说什么从UIApplication开始下传了,这边只说说视图层的传递:
事件传递:父视图往子视图传递,这个图传递如下
点击B:A->B
点击D:A->C->D
怎么验证这个说法,最简单的,关闭父视图的userInteractionEnabled,这时候点击子视图无效,但是关闭子视图响应,父视图仍然可以做成响应
2、响应事件处理
在判断事件响应的过程中主要用到两个函数,决定哪个视图来响应事件,是通过不断递归调用View中的 - (UIView *)hitTest: withEvent: 方法和 -(BOOL)pointInside: withEvent: 方法来实现的
我们通过打印log来分析系统方法响应顺序,效果图代码如下:
RedView代码:
#import "RedView.h"
@implementation RedView
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self addLabel];
}
return self;
}
- (void)addLabel
{
UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, self.bounds.size.width, 20)];
label.text = @"A View";
[self addSubview:label];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"A_touchesBegan");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
NSLog(@"A_touchesMoved");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
NSLog(@"A_touchesEnded");
}
//返回最适合处理事件的视图,最好在父视图中指定子视图的响应
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"进入A_View---hitTest withEvent ---");
UIView * view = [super hitTest:point withEvent:event];
NSLog(@"离开A_View--- hitTest withEvent ---hitTestView:%@",view);
return view;
}
//判断一个点是否落在自己的视图范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
NSLog(@"A_view--- pointInside withEvent ---");
BOOL isInside = [super pointInside:point withEvent:event];
NSLog(@"A_view--- pointInside withEvent --- isInside:%d",isInside);
return isInside;
}
@end
点击空白处,log如下:
父视图通过调用自身pointInside方法判断点是否落在本视图上,落在本视图就调用其子类的hitTest方法来判断谁来响应该事件,假如我们在A视图上改一个跟B视图一样大小的C视图log如下:
由此我们可以大致得出hitTest代码如下:
//返回最适合处理事件的视图,最好在父视图中指定子视图的响应
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.userInteractionEnabled || !self.hidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
CGPoint subPoint = [subView convertPoint:point fromView:self];
UIView *bestView = [subView hitTest:subPoint withEvent:event];
if (bestView) {
return bestView;
}
}
return self;
}
return nil;
}
3、重写这两个函数的作用
假如我们需要响应一个圆形区域的点击事件,我们可以通过重写该视图的pointInside方法如下:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
//首先调用父类的方法确定点击的区域确实在按钮的区域中
BOOL res = [super pointInside:point withEvent:event];
if (res) {
//绘制一个圆形path
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:self.bounds];
if ([path containsPoint:point]) {
//如果在path区域内,返回YES
return YES;
}
return NO;
}
return NO;
}
除此之外,我们还可以扩大、 屏蔽某视图的响应区域等等,大家自由发挥
4、视图不响应检查要点
Tips:有时候发现一个视图无法响应点击事件,可以检查下面几项
1、hidden = YES 视图被隐藏
2、userInteractionEnabled = NO 不接受响应事件
3、alpha <= 0.01,透明视图不接收响应事件
4、子视图超出父视图范围
5、需响应视图被其他视图盖住
6、是否重写了其父视图以及自身的hitTest方法
7、是否重写了其父视图以及自身的pointInside方法