Hit-Test和響應(yīng)鏈
什么叫 hit-test view?文檔說:The lowest view in the view hierarchy that contains the touch point becomes the hit-test view,我的理解是:當(dāng)你點(diǎn)擊了屏幕上的某個(gè)view,這個(gè)動作由硬件層傳導(dǎo)到操作系統(tǒng),然后又從底層封裝成一個(gè)事件(Event),從keyWindow開始順著view的層級往上傳導(dǎo),一直要找到含有這個(gè)點(diǎn)擊點(diǎn)且層級最高的view來響應(yīng)事件,這個(gè)view就是hit-test view。
如果在hit-test中調(diào)用默認(rèn)的super hittest:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView * view = [super hitTest:point withEvent:event];
return view;
}
則其內(nèi)在的行為等價(jià)于:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
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;
}
如果有某個(gè)view的兩個(gè)子view位置重疊,那最高層(邏輯最靠近手指的)view是view subviews數(shù)組的最后一個(gè)元素,只要尋找是從數(shù)組的第一個(gè)元素開始遍歷,hit-test view的邏輯依然是有效的。(即reverseObjectEnumerator逆序枚舉中的首個(gè)元素執(zhí)行到return self的優(yōu)先級最高。)

找到hit-test view后,它會有最高的優(yōu)先權(quán)去響應(yīng)逐級傳遞上來的Event,如它不能響應(yīng)就會傳遞給它的superview,依此類推,一直傳遞到UIApplication都無響應(yīng)者,這個(gè)Event就會被系統(tǒng)丟棄了。

可以看到,持有View的View Controller會先于super View得到響應(yīng)時(shí)間的機(jī)會。
應(yīng)用舉例
1、擴(kuò)大UIButton的響應(yīng)熱區(qū)
重載UIButton的-(BOOL)pointInside: withEvent:方法,讓Point即使落在Button的Frame外圍也返回YES。
//in custom button .m
//overide this method
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
}
CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
CGRect hitTestingBounds = bounds;
if (minimumHitTestWidth > bounds.size.width) {
hitTestingBounds.size.width = minimumHitTestWidth;
hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2;
}
if (minimumHitTestHeight > bounds.size.height) {
hitTestingBounds.size.height = minimumHitTestHeight;
hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2;
}
return hitTestingBounds;
}
2、子view超出了父view的bounds響應(yīng)事件
項(xiàng)目中常常遇到button已經(jīng)超出了父view的范圍但仍需可點(diǎn)擊的情況,比如自定義Tabbar中間的大按鈕,如在底部TabberBar中間放置宇哥大按鈕,點(diǎn)擊超出Tabbar bounds的區(qū)域也需要響應(yīng),此時(shí)重載父view的-(UIView *)hitTest: withEvent:方法,去掉點(diǎn)擊必須在父view內(nèi)的判斷,然后子view就能成為 hit-test view用于響應(yīng)事件了。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
/**
* 此注釋掉的方法用來判斷點(diǎn)擊是否在父View Bounds內(nèi),
* 如果不在父view內(nèi),就會直接不會去其子View中尋找HitTestView,return 返回
*/
// 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;
}
3、ScrollView page滑動
當(dāng)使用scrollview進(jìn)行分頁顯示的時(shí)候(PageEnable = YES),分頁的寬度是scrollview的寬度,通常我們使用scrollview全屏顯示內(nèi)容,如果我們需要半屏幕寬或者其他小于屏幕寬度的分頁,則需要以下步驟:
- 設(shè)置你的UIScrollView的寬度為Width/2;
- 開啟分頁模式:self.pagingEnabled = YES;
- 關(guān)閉self.clipsToBounds = NO; 這樣超出范圍的視圖也會顯示。
- 然后重寫UIScrollView所在的parentView的hitTest事件,讓其返回值是UIScrollView對象. 此舉可以使scrollview兩側(cè)的區(qū)域也能響應(yīng)scrollview的滑動事件。
第四部的代碼如下:
//in scrollView.superView .m
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitTestView = [super hitTest:point withEvent:event];
if (hitTestView) {
hitTestView = self.scrollView;
}
return hitTestView;
}