App通過響應(yīng)者對象來接收和處理事件坪仇,響應(yīng)者對象都是UIResponder的子類對象拉背,常見的UIView芍碧,UIVieController和UIApplication都是UIResponder的子類.響應(yīng)者接收事件原始數(shù)據(jù)之后必須進行事件處理或者轉(zhuǎn)發(fā)給其他的響應(yīng)者對象.
當前App應(yīng)用程接收到一個事件飞醉,UIKit會自動找到最合適的響應(yīng)者對象,也就是常說的第一響應(yīng)者.UIKit定義了響應(yīng)者對象是如何進行事件轉(zhuǎn)發(fā)的奠涌,常見的響應(yīng)者鏈:
第一響應(yīng)者
對于不同類型的事件,UIKit根據(jù)事件類型將事件對象傳遞給對應(yīng)的響應(yīng)者.
觸摸事件:第一響應(yīng)者就是觸摸發(fā)生的視圖.
焦點事件:第一響應(yīng)者就是具有焦點的容器控件.
搖晃事件:需要自己或UIKit指定第一響應(yīng)者.
遠程事件:需要自己或UIKit指定第一響應(yīng)者.
編輯菜單消息事件:需要自己或UIKit指定第一響應(yīng)者.
加速度計磷杏,陀螺儀及磁強計有關(guān)的運動事件不遵循響應(yīng)鏈.
觸摸事件響應(yīng)者
UIKit通過基于視圖的hit-testing來確認觸摸事件的發(fā)生.UIKit會按照視圖的層級溜畅,逐層的按照觸摸視圖的位置和視圖的bouds進行對比hitTest(_:with:)
來尋找包含觸摸事件最深層次的視圖,這個視圖就會成為觸摸事件的第一響應(yīng)者.
如果父視圖不能響應(yīng)觸摸事件极祸,那么所有的子視圖也不會響應(yīng)慈格,即使子視圖的位置超出父視圖的bounds也不會響應(yīng).
- (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將不會執(zhí)行.
響應(yīng)者鏈
響應(yīng)者鏈通常是UIView組成的視圖層級浴捆,假設(shè)第一響應(yīng)者對象是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(丟棄)
實際應(yīng)用
Hit-Tesing 過程是從上向下(從父視圖到子視圖)遍歷页眯;Response Chain是 事件處理傳遞是從下向上(從子視圖到父視圖)傳遞。
關(guān)于事件響應(yīng)者鏈有一個場景項目會經(jīng)常遇到厢呵,按鈕大小窝撵,經(jīng)常會被產(chǎn)品經(jīng)理要求按鈕點擊不靈敏,要求擴大點擊區(qū)域.
1.自定義按鈕襟铭,重寫pointInside事件:
@implementation FEButton
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect bounds = self.bounds;
//若原熱區(qū)小于44x44碌奉,則放大熱區(qū)短曾,否則保持原大小不變
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.項目中經(jīng)常會用到圓角,這個時候有人告訴你道批,兄弟错英,我只想點擊圓角內(nèi)的區(qū)域響應(yīng)事件,旁邊的空白不希望響應(yīng).補習了基本的數(shù)學知識之后隆豹,代碼修改如下:
@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---圓形擴大點擊區(qū)域");
}
- (void)buttonAction2:(UIButton *)sender {
NSLog(@"Runtime擴大點擊");
}
參考鏈接
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