1. 事件响应的过程
在iOS中的view之间逐层叠加,当点击了屏幕上的某个view时,这个点击动作会由硬件层传导到操作系统并生成一个事件(Event),这个事件顺着view的层级由下往上传导,直至找到包含有这个点击点、层级最高、且可与用户交互的view来响应这个事件。
2. 响应链中涉及的方法
- UIView中的hitTest方法、pointInside方法
// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
// default returns YES if point is in bounds
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
// 点位转换相关方法
- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;
hitTest方法的实现:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//系统默认会忽略isUserInteractionEnabled设置为NO、隐藏、alpha小于等于0.01的视图
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}
- UIResponder中的touchesBegan、touchesMoved、touchesEnded等方法
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
// 这几个方法比较常用,在此不再敖述;
// 当然,UIResponder中不止这三个响应事件的方法,本文仅以touches的这三个方法为例。
- 示例
为了使我们更好的理解事件响应过程中,上述UIView与UIResponder这几个方法的执行过程,我们用以下图示例(示例参考文章)进行说明,图中视图ABCDE(UIView型)之间的层次关系是self.view(A(B, C(D, E))):
以下代码是在A视图中都重写我们需要观察的几个父类方法,BCDE中需要重写的代码以此类推:
/*
* 例如:A中重写父类方法的代码,
*/
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"AView ---->> hitTest:withEvent: ---");
UIView * view = [super hitTest:point withEvent:event];
NSLog(@"AView <<--- hitTest:withEvent: --- /n hitTestView:%@", view);
return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
NSLog(@"AView --->> pointInside:withEvent: ---");
BOOL isInside = [super pointInside:point withEvent:event];
NSLog(@"AView <<--- pointInside:withEvent: --- isInside:%d", isInside);
return isInside;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"AView touchesBegan");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
NSLog(@"AView touchesMoved");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
NSLog(@"AView touchesEnded");
}
当点击了一下B视图所在区域时,Xcode输出log如下:
3. hitTest方法的应用
3.1 改变UIButton的响应热区
具体的说改变视图的响应热区,主要是在pointInside方法中完成的,QiShare关于改变热区的文章中有过描述。但是hitTest、pointInside同属响应链中方法,如果有需求,也可以在hitTest中返回一个确定的view。
3.2 view超出superView的bounds仍能响应事件
如图,在黄色superView上添加一个UIButton,UIButton上半部分超出superView。正常的情况下点击红框区域时,UIButton是无法响应点击事件的,要让红框区域内的UIButton仍能响应点击事件,需要我们重写superView的hitTest方法。
#import "BeyondBoundsOfView.h"
@interface BeyondBoundsOfView ()
@property (nonatomic, strong) UIButton *button;
@end
@implementation BeyondBoundsOfView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_button = [UIButton buttonWithType:UIButtonTypeSystem];
[_button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[_button setTitle:@"UIButton" forState:UIControlStateNormal];
[_button setBackgroundColor:[UIColor lightGrayColor]];
_button.frame = CGRectMake(0, 0, 80, 80);
[self addSubview:_button];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGSize size = self.frame.size;
_button.center = CGPointMake(size.width / 2, 0);
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
for (UIView *subview in self.subviews) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return nil;
}
@end