級別: ★★☆☆☆
標籤:「iOS」「hitTest」「pointInside」
做者: dac_1033
審校: QiShare團隊
php
在iOS中的view之間逐層疊加,當點擊了屏幕上的某個view時,這個點擊動做會由硬件層傳導到操做系統並生成一個事件(Event),這個事件順着view的層級由下往上傳導,直至找到包含有這個點擊點、層級最高、且可與用戶交互的view來響應這個事件。事件的傳遞過程官網有圖解: git
// 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、pointInside兩個方法配合的情形下,向下傳遞;
- hitTest:withEvent:在內部首先會判斷該視圖是否能響應觸摸事件,若是不能響應,返回nil,表示該視圖不響應此觸摸事件。而後再調用pointInside:withEvent:(該方法用來判斷點擊事件發生的位置是否處於當前視圖範圍內)。若是pointInside:withEvent:返回NO,那麼hiteTest:withEvent:也直接返回nil;
- 若是pointInside:withEvent:返回YES,則向當前視圖的全部子視圖發送hitTest:withEvent:消息,全部子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖,即從subviews數組的末尾向前遍歷。直到有子視圖返回非空對象或者所有子視圖遍歷完畢;若第一次有子視圖返回非空對象,則 hitTest:withEvent:方法返回此對象,處理結束;如全部子視圖都返回非,則hitTest:withEvent:方法返回該視圖自身。
// 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的這三個方法爲例。
複製代碼
如下代碼是在A視圖中都重寫咱們須要觀察的幾個父類方法,BCDE中須要重寫的代碼以此類推:github
/*
* 例如: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以下:算法
在示例中能夠發現響應鏈中所涉及方法的執行過程,有如下特色數組
- 當UIView中的isUserInteractionEnabled = NO、isHidden = YES、alpha <= 0.01時,hitTest方法不會被調用;
- UIResponder 中的touches三個方法都是發生在找到最終的響應事件的view以後;
- 二是尋找hit-test view的事件鏈傳導了兩遍,具體緣由不明;
改變UIButton的響應熱區 具體的說改變視圖的響應熱區,主要是在pointInside方法中完成的,QiShare關於改變熱區的文章中有過描述。可是hitTest、pointInside同屬響應鏈中方法,若是有需求,也能夠在hitTest中返回一個***肯定的view***。bash
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
複製代碼
上面代碼中關鍵的一行: CGPoint convertedPoint = [subview convertPoint:point fromView:self]; 獲取到convertedPoint對咱們循環調用子view的hitTest很關鍵。app
工程源碼GitHub地址
ide
小編微信:可加並拉入《QiShare技術交流羣》。測試
關注咱們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公衆號)
推薦文章:
iOS 關於tabBar的幾處筆記
算法小專欄:談談大O表示法
iOS UIWebView、WKWebView注入Cookie
Cookie簡介
iOS 圖標&啓動圖生成器(一)
算法小專欄:「D&C思想」與「快速排序」
奇舞週刊