Hit-Test 原理及應(yīng)用案例

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

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


responder chain

可以看到,持有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;
}

參考:iOS事件響應(yīng)鏈中Hit-Test View的應(yīng)用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容