App通过响应者对象来接收和处理事件,响应者对象都是UIResponder的子类对象,常见的UIView,UIVieController和UIApplication都是UIResponder的子类.响应者接收事件原始数据之后必须进行事件处理或者转发给其他的响应者对象.
当前App应用程接收到一个事件,UIKit会自动找到最合适的响应者对象,也就是常说的第一响应者.UIKit定义了响应者对象是如何进行事件转发的,常见的响应者链:
事件响应者链.png第一响应者
对于不同类型的事件,UIKit根据事件类型将事件对象传递给对应的响应者.
触摸事件:第一响应者就是触摸发生的视图.
焦点事件:第一响应者就是具有焦点的容器控件.
摇晃事件:需要自己或UIKit指定第一响应者.
远程事件:需要自己或UIKit指定第一响应者.
编辑菜单消息事件:需要自己或UIKit指定第一响应者.
加速度计,陀螺仪及磁强计有关的运动事件不遵循响应链.
触摸事件响应者
UIKit通过基于视图的hit-testing来确认触摸事件的发生.UIKit会按照视图的层级,逐层的按照触摸视图的位置和视图的bouds进行对比hitTest(_:with:)
来寻找包含触摸事件最深层次的视图,这个视图就会成为触摸事件的第一响应者.
如果父视图不能响应触摸事件,那么所有的子视图也不会响应,即使子视图的位置超出父视图的bounds也不会响应.
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
当视图hidden = YES,禁用用户操作(userInteractionEnabled=YES,透明度小于0.01的时候讲hitTest将不会执行.
响应者链
响应者链通常是UIView组成的视图层级,假设第一响应者对象是UIView是视图控制器的根view,next responder是它的试图控制器,或者是它的父视图.
如果视图控制器是window的根控制器,next responder是window对象,如果控制器是被presented出来的,那么next responder是它的父控制器.最终找到UIWindow对象.
UIWiddow的next responder是UIApplication对象.
UIApplication的next responder是app delegate.
寻找顺序如下:
First Responser --> The Window --> The Application --> nil(丢弃)
实际应用
关于事件响应者链有一个场景项目会经常遇到,按钮大小,经常会被产品经理要求按钮点击不灵敏,要求扩大点击区域.
1.自定义按钮,重写pointInside事件:
@implementation FEButton
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect bounds = self.bounds;
//若原热区小于44x44,则放大热区,否则保持原大小不变
CGFloat widthDelta = MAX(44.0 - bounds.size.width, 0);
CGFloat heightDelta = MAX(44.0 - bounds.size.height, 0);
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
NSLog(@"FlyElephant---被点击了:%@----点击的点:%@",NSStringFromCGRect(bounds), NSStringFromCGPoint(point));
return CGRectContainsPoint(bounds, point);
}
@end
2.项目中经常会用到圆角,这个时候有人告诉你,兄弟,我只想点击圆角内的区域响应事件,旁边的空白不希望响应.补习了基本的数学知识之后,代码修改如下:
@implementation CircleButton
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGFloat halfWidth = self.bounds.size.width / 2;
CGFloat xDistance = point.x - halfWidth;
CGFloat yDistance = point.y - halfWidth;
CGFloat radius = sqrt(xDistance * xDistance + yDistance * yDistance);
NSLog(@"HaldWidth:%f---point:%@---x轴距离:%f---y轴距离:%f--半径:%f",halfWidth,NSStringFromCGPoint(point),xDistance,yDistance,radius);
return radius <= halfWidth;
}
@end
3.自定义太多按钮,心好累,最终还是扩展UIButton,提供工作效率.
@interface UIButton (FlyElephant)
@property(nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;
@end
#import "UIButton+FlyElephant.h"
#import <objc/runtime.h>
@implementation UIButton (FlyElephant)
static const NSString *KEY_HIT_TEST_EDGE_INSETS = @"HitTestEdgeInsets";
-(void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets {
NSValue *value = [NSValue value:&hitTestEdgeInsets withObjCType:@encode(UIEdgeInsets)];
objc_setAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(UIEdgeInsets)hitTestEdgeInsets {
NSValue *value = objc_getAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS);
if(value) {
UIEdgeInsets edgeInsets; [value getValue:&edgeInsets]; return edgeInsets;
}else {
return UIEdgeInsetsZero;
}
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || !self.enabled || self.hidden) {
return [super pointInside:point withEvent:event];
}
CGRect relativeFrame = self.bounds;
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
@end
UI测试代码:
- (void)setUp {
UIView *bgView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
bgView.backgroundColor = [UIColor redColor];
[self.view addSubview:bgView];
FEButton *button = [[FEButton alloc] initWithFrame:CGRectMake(20, 20, 20, 20)];
button.backgroundColor = [UIColor greenColor];
[bgView addSubview:button];
UIView *bgView1 = [[UIView alloc] initWithFrame:CGRectMake(100, 300, 100, 100)];
bgView1.backgroundColor = [UIColor redColor];
[self.view addSubview:bgView1];
CircleButton *button1 = [[CircleButton alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
button1.backgroundColor = [UIColor greenColor];
button1.clipsToBounds = YES;
button1.layer.cornerRadius = 50;
[button1 addTarget:self action:@selector(buttonAction1:) forControlEvents:UIControlEventTouchUpInside];
[bgView1 addSubview:button1];
UIView *bgView2 = [[UIView alloc] initWithFrame:CGRectMake(100, 500, 100, 100)];
bgView2.backgroundColor = [UIColor redColor];
[self.view addSubview:bgView2];
UIButton *button2 = [[UIButton alloc] initWithFrame:CGRectMake(20, 20, 20, 20)];
button2.backgroundColor = [UIColor greenColor];
button2.hitTestEdgeInsets = UIEdgeInsetsMake(-20, -20, -20, -20);
[button2 addTarget:self action:@selector(buttonAction2:) forControlEvents:UIControlEventTouchUpInside];
[bgView2 addSubview:button2];
}
- (void)buttonAction1:(UIButton *)sender {
NSLog(@"FlyElephant---圆形扩大点击区域");
}
- (void)buttonAction2:(UIButton *)sender {
NSLog(@"Runtime扩大点击");
}
FlyElephant.png
参考链接
https://developer.apple.com/documentation/uikit/understanding_event_handling_responders_and_the_responder_chain
https://stackoverflow.com/questions/808503/uibutton-making-the-hit-area-larger-than-the-default-hit-area
http://www.cnblogs.com/wengzilin/p/4249847.html?utm_source=tuicool&utm_medium=referral
网友评论