1. 事件響應(yīng)的過程
在iOS中的view之間逐層疊加蛀醉,當點擊了屏幕上的某個view時案训,這個點擊動作會由硬件層傳導(dǎo)到操作系統(tǒng)并生成一個事件(Event)勒魔,這個事件順著view的層級由下往上傳導(dǎo)建钥,直至找到包含有這個點擊點壶笼、層級最高、且可與用戶交互的view來響應(yīng)這個事件腿宰。
2. 響應(yīng)鏈中涉及的方法
- 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;
// 點位轉(zhuǎn)換相關(guān)方法
- (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方法的實現(xiàn):
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//系統(tǒng)默認會忽略isUserInteractionEnabled設(shè)置為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;
}
- 點擊事件會在hitTest甩挫、pointInside兩個方法的配合下,向下傳遞椿每;
- 在hitTest:withEvent:內(nèi)部首先會判斷該視圖是否能響應(yīng)觸摸事件伊者,如果不能響應(yīng)英遭,返回nil,表示該視圖不響應(yīng)此觸摸事件亦渗。然后再調(diào)用pointInside:withEvent:(該方法用來判斷點擊事件發(fā)生的位置是否處于當前視圖處理范圍)挖诸。如果pointInside:withEvent:返回NO,那么hiteTest:withEvent:也直接返回nil央碟;
- 如果pointInside:withEvent:返回YES税灌,則向當前視圖的所有子視圖發(fā)送hitTest:withEvent:消息,所有子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖亿虽,即從subviews數(shù)組的末尾向前遍歷菱涤。直到有子視圖返回非空對象或者全部子視圖遍歷完畢;若第一次有子視圖返回非空對象洛勉,則 hitTest:withEvent:方法返回此對象粘秆,處理結(jié)束;如所有子視圖都返回非收毫,則hitTest:withEvent:方法返回該視圖自身攻走。
- 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中不止這三個響應(yīng)事件的方法摘符,本文僅以touches的這三個方法為例。
- 示例
為了使我們更好的理解事件響應(yīng)過程中策吠,上述UIView與UIResponder這幾個方法的執(zhí)行過程逛裤,我們用以下圖示例(示例參考文章)進行說明,圖中視圖ABCDE(UIView型)之間的層次關(guān)系是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視圖所在區(qū)域時,Xcode輸出log如下:
在示例中可以發(fā)現(xiàn)響應(yīng)鏈中所涉及方法的執(zhí)行過程蝙砌,有以下特點
- 當UIView中的isUserInteractionEnabled = NO、isHidden = YES坤溃、alpha <= 0.01時拍霜,hitTest方法不會被調(diào)用;
- UIResponder 中的touches三個方法都是發(fā)生在找到最終的響應(yīng)事件的view之后薪介;
- 二是尋找hit-test view的事件鏈傳導(dǎo)了兩遍,具體原因不明;
- 請自行修改兩個方法的返回值越驻,來實驗汁政。
3. hitTest方法的應(yīng)用
3.1 改變UIButton的響應(yīng)熱區(qū)
具體的說改變視圖的響應(yīng)熱區(qū)道偷,主要是在pointInside方法中完成的,QiShare關(guān)于改變熱區(qū)的文章中有過描述记劈。但是hitTest勺鸦、pointInside同屬響應(yīng)鏈中方法,如果有需求目木,也可以在hitTest中返回一個確定的view换途。
3.2 view超出superView的bounds仍能響應(yīng)事件
如圖,在黃色superView上添加一個UIButton刽射,UIButton上半部分超出superView军拟。正常的情況下點擊紅框區(qū)域時,UIButton是無法響應(yīng)點擊事件的誓禁,要讓紅框區(qū)域內(nèi)的UIButton仍能響應(yīng)點擊事件懈息,需要我們重寫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
上面代碼中關(guān)鍵的一行:
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
獲取到convertedPoint對我們循環(huán)調(diào)用子view的hitTest很關(guān)鍵摹恰。