hitTest由淺入深

本文將從如下幾個方面來介紹它:

  • 什么是hitTest
  • hitTest、響應(yīng)者鏈和觸摸事件的先后順序是什么
  • hitTest實現(xiàn)思路以及模仿
  • hitTest使用場景

1.什么是hitTest

按照蘋果官方的解釋如下:

  Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.
  This method traverses the view hierarchy by calling the [pointInside:withEvent:] method of each subview to determine which subview should receive a touch event. If [pointInside:withEvent:] returns `YES`, then the subview’s hierarchy is similarly traversed until the frontmost view containing the specified point is found. If a view does not contain the point, its branch of the view hierarchy is ignored. You rarely need to call this method yourself, but you might override it to hide touch events from subviews.
  This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than `0<wbr>.01`. This method does not take the view’s content into account when determining a hit. Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content.Points that lie outside the receiver’s bounds are never reported as hits, even if they actually lie within one of the receiver’s subviews. This can occur if the current view’s [clipsToBounds] property is set to `NO` and the affected subview extends beyond the view’s bounds.

大概意思就是,hitTest會查找視圖層級樹上最遠的視圖,看它是否能包含這個點(通過pointInside:withEvent:實現(xiàn)),如果包含就從它的子視圖里面查找,按照由遠及近的順序(先查找最后添加的subview),如果事件觸發(fā)點在該視圖里面則優(yōu)先返回。然后hitTest遇到下面幾種情況也不會觸發(fā):

  • hidden = YES
  • userInteractionEnabled = NO
  • alpha <= 0.01
  • 父視圖clipsToBounds = NO 且子視圖超出父視圖的bounds所在范圍

如下圖:

圖1.1.jpg

添加順序為A - B - C - D - E。按照官方說法,可以得出結(jié)論是:
無論點擊A還是B都會先去查找B視圖是否能夠響應(yīng)點擊(包含該點),因為B在視圖樹中比A后添加。
我們可以看下調(diào)用棧順序。
先點擊A:

2020-11-29 11:17:42.727702+0800 demo22[22576:1124696] BView_hitTest_start
2020-11-29 11:17:42.727910+0800 demo22[22576:1124696] pointInside:-[BView pointInside:withEvent:]
2020-11-29 11:17:42.728090+0800 demo22[22576:1124696] BView_hitTest_end_(null)
2020-11-29 11:17:42.728253+0800 demo22[22576:1124696] AView_hitTest_start
2020-11-29 11:17:42.728440+0800 demo22[22576:1124696] pointInside:-[AView pointInside:withEvent:]
2020-11-29 11:17:42.734152+0800 demo22[22576:1124696] CView_hitTest_start
2020-11-29 11:17:42.734318+0800 demo22[22576:1124696] pointInside:-[CView pointInside:withEvent:]
2020-11-29 11:17:42.734445+0800 demo22[22576:1124696] CView_hitTest_end_(null)
2020-11-29 11:17:42.734674+0800 demo22[22576:1124696] AView_hitTest_end_<AView: 0x7fc8ca606f40; frame = (20 120; 120 120); gestureRecognizers = <NSArray: 0x6000002c3d20>; layer = <CALayer: 0x600000cb2660>>
2020-11-29 11:17:42.736005+0800 demo22[22576:1124696] touchesBegan:-[AView touchesBegan:withEvent:]
2020-11-29 11:17:42.832949+0800 demo22[22576:1124696] tapAView

先點擊B:

2020-11-29 11:21:46.151116+0800 demo22[22576:1124696] BView_hitTest_start
2020-11-29 11:21:46.151283+0800 demo22[22576:1124696] pointInside:-[BView pointInside:withEvent:]
2020-11-29 11:21:46.151727+0800 demo22[22576:1124696] EView_hitTest_start
2020-11-29 11:21:46.152238+0800 demo22[22576:1124696] pointInside:-[EView pointInside:withEvent:]
2020-11-29 11:21:46.152684+0800 demo22[22576:1124696] EView_hitTest_end_(null)
2020-11-29 11:21:46.153100+0800 demo22[22576:1124696] DView_hitTest_start
2020-11-29 11:21:46.153428+0800 demo22[22576:1124696] pointInside:-[DView pointInside:withEvent:]
2020-11-29 11:21:46.158206+0800 demo22[22576:1124696] DView_hitTest_end_(null)
2020-11-29 11:21:46.158436+0800 demo22[22576:1124696] BView_hitTest_end_<BView: 0x7fc8ca608ef0; frame = (20 300; 120 120); gestureRecognizers = <NSArray: 0x6000002c34b0>; layer = <CALayer: 0x600000cc5b00>>
2020-11-29 11:21:46.159338+0800 demo22[22576:1124696] touchesBegan:-[BView touchesBegan:withEvent:]
2020-11-29 11:21:46.220054+0800 demo22[22576:1124696] tapBView

通過查看調(diào)用棧我們可以發(fā)現(xiàn)上述結(jié)論的正確性。

2.hitTest、響應(yīng)鏈和手勢的先后順序是什么

我們可以再點擊C看下調(diào)用棧(tapCView為手勢action):

2020-11-29 12:38:08.558449+0800 demo22[64342:1317416] BView_hitTest_start
2020-11-29 12:38:08.558588+0800 demo22[64342:1317416] pointInside:-[BView pointInside:withEvent:]
2020-11-29 12:38:08.558714+0800 demo22[64342:1317416] BView_hitTest_end_(null)
2020-11-29 12:38:08.558859+0800 demo22[64342:1317416] AView_hitTest_start
2020-11-29 12:38:08.558981+0800 demo22[64342:1317416] pointInside:-[AView pointInside:withEvent:]
2020-11-29 12:38:08.559104+0800 demo22[64342:1317416] CView_hitTest_start
2020-11-29 12:38:08.563553+0800 demo22[64342:1317416] pointInside:-[CView pointInside:withEvent:]
2020-11-29 12:38:08.563766+0800 demo22[64342:1317416] CView_hitTest_end_<CView: 0x7fa7426043b0; frame = (10 20; 60 60); gestureRecognizers = <NSArray: 0x600000a46310>; layer = <CALayer: 0x60000047fb20>>
2020-11-29 12:38:08.563885+0800 demo22[64342:1317416] AView_hitTest_end_<CView: 0x7fa7426043b0; frame = (10 20; 60 60); gestureRecognizers = <NSArray: 0x600000a46310>; layer = <CALayer: 0x60000047fb20>>
2020-11-29 12:38:08.564989+0800 demo22[64342:1317416] touchesBegan:-[CView touchesBegan:withEvent:]
2020-11-29 12:38:08.565174+0800 demo22[64342:1317416] touchesBegan:-[AView touchesBegan:withEvent:]
2020-11-29 12:38:08.565347+0800 demo22[64342:1317416] touchesBegan-[ViewController touchesBegan:withEvent:]
2020-11-29 12:38:08.642938+0800 demo22[64342:1317416] tapCView
2020-11-29 12:38:08.643475+0800 demo22[64342:1317416] touchesBegan:-[CView touchesCancelled:withEvent:]

同樣的,hitTest會按照視圖樹去查找,始終查找的是最遠的那個View.查找順序如下圖:

image.png

首先,會找到B,然后判斷該點是否在B內(nèi),判斷為不在,然后再去查找A是否包含;如果包含,則進一步查找A的子視圖C。如果C能夠響應(yīng),則C開始出發(fā)事件響應(yīng)。即觸發(fā)UIResponder的touchesBegan事件,然后往視圖層級鏈向上拋出事件。從C -> A -> controller.view - > controller -> window -> UIApplication -> 事件丟棄
所以,hitTest只是來查找能夠響應(yīng)點擊事件的View,然后該View觸發(fā)事件響應(yīng),然后沿著視圖層級鏈往上傳遞。剛好,是沿著相反的方向。
也可以看出,手勢是基于UIResponser 的touch事件封裝,優(yōu)先級比touch事件高
總結(jié):

  • hitTest是查找響應(yīng)者鏈的方法,順序是由遠及近。(優(yōu)先查找父視圖上最遠的子視圖)
  • 響應(yīng)者鏈當(dāng)然就是由由遠及近。
  • 觸摸事件順序剛好和響應(yīng)者鏈相反。

3.hitTest實現(xiàn)思路以及模仿

我們先點擊E查看下調(diào)用棧:

2020-11-29 11:50:10.132728+0800 demo22[37861:1197365] BView_hitTest_start
2020-11-29 11:50:10.132919+0800 demo22[37861:1197365] pointInside:-[BView pointInside:withEvent:]
2020-11-29 11:50:10.133119+0800 demo22[37861:1197365] EView_hitTest_start
2020-11-29 11:50:10.133323+0800 demo22[37861:1197365] pointInside:-[EView pointInside:withEvent:]
2020-11-29 11:50:10.134105+0800 demo22[37861:1197365] EView_hitTest_end_<EView: 0x7fd5456065c0; frame = (40 0; 60 40); alpha = 0.5; gestureRecognizers = <NSArray: 0x6000033091d0>; layer = <CALayer: 0x600003d2e620>>
2020-11-29 11:50:10.134820+0800 demo22[37861:1197365] BView_hitTest_end_<EView: 0x7fd5456065c0; frame = (40 0; 60 40); alpha = 0.5; gestureRecognizers = <NSArray: 0x6000033091d0>; layer = <CALayer: 0x600003d2e620>>
2020-11-29 11:50:10.143368+0800 demo22[37861:1197365] touchesBegan:-[EView touchesBegan:withEvent:]
2020-11-29 11:50:10.143544+0800 demo22[37861:1197365] touchesBegan:-[BView touchesBegan:withEvent:]
2020-11-29 11:50:10.219565+0800 demo22[37861:1197365] tapEView

首先從controller.view的最遠端subview開始(即B),接著再是B視圖的最遠端
(即E).綜上所述,我們可以寫一個View的父視圖,重寫它的hitTest方法如下:

#import "MyRootView.h"

@implementation MyRootView

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //return [super hitTest:point withEvent:event];
    if (self.hidden || !self.userInteractionEnabled || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *obj in self.subviews.reverseObjectEnumerator) {
            CGPoint convertPoint = [self convertPoint:point toView:obj];
            UIView *subview = [obj hitTest:convertPoint withEvent:event];//這里是個遞歸
            if (subview) {
                return subview;
            }
        }
        return self;
    }
    return nil;
}

@end

然后把A,B,C,D,E的父類都指向MyRootView,再次點擊E的調(diào)用棧如下:

2020-11-29 12:05:18.054983+0800 demo22[48256:1243168] BView_hitTest_start
2020-11-29 12:05:18.055142+0800 demo22[48256:1243168] pointInside:-[BView pointInside:withEvent:]
2020-11-29 12:05:18.055309+0800 demo22[48256:1243168] EView_hitTest_start
2020-11-29 12:05:18.055470+0800 demo22[48256:1243168] pointInside:-[EView pointInside:withEvent:]
2020-11-29 12:05:18.056175+0800 demo22[48256:1243168] EView_hitTest_end_<EView: 0x7fdbe8410210; frame = (40 0; 60 40); alpha = 0.5; gestureRecognizers = <NSArray: 0x600002a5dfe0>; layer = <CALayer: 0x600002470f60>>
2020-11-29 12:05:18.056852+0800 demo22[48256:1243168] BView_hitTest_end_<EView: 0x7fdbe8410210; frame = (40 0; 60 40); alpha = 0.5; gestureRecognizers = <NSArray: 0x600002a5dfe0>; layer = <CALayer: 0x600002470f60>>
2020-11-29 12:05:18.059365+0800 demo22[48256:1243168] touchesBegan:-[EView touchesBegan:withEvent:]
2020-11-29 12:05:18.059628+0800 demo22[48256:1243168] touchesBegan:-[BView touchesBegan:withEvent:]
2020-11-29 12:05:18.140683+0800 demo22[48256:1243168] tapEView

我們可以對比發(fā)現(xiàn),可官方的調(diào)用棧簡直是一模模一樣樣,是不是感覺很神奇呢?

4.hitTest使用場景

  • 場景1

有時候我們會碰到如下情況:


image.png

子視圖bounds超出父視圖的容器,如果不加處理這時候是無法響應(yīng)點擊事件的。
那么這時候我們就要重寫父視圖的hitTest方法,把最佳響應(yīng)視圖View確定在中間Button上。例如:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGPoint convertPoint = [self convertPoint:point toView:_centerButton];
    if ([_centerButton pointInside:convertPoint withEvent:event]) {
        return _centerButton;
    }
    return [super hitTest:point withEvent:event];
}
  • 場景2 - 事件穿透

比如圖1.1中,A和C有重疊部分,我們希望的是“點擊C的時候,把事件交給A來處理”。
那么我們可以重寫C的hitTest方法如下:

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

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