hitTest由淺入深

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

  • 什么是hitTest
  • hitTest修己、響應(yīng)者鏈和觸摸事件的先后順序是什么
  • hitTest實(shí)現(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會查找視圖層級樹上最遠(yuǎn)的視圖醇锚,看它是否能包含這個點(diǎn)(通過pointInside:withEvent:實(shí)現(xiàn)),如果包含就從它的子視圖里面查找改艇,按照由遠(yuǎn)及近的順序(先查找最后添加的subview)坟岔,如果事件觸發(fā)點(diǎn)在該視圖里面則優(yōu)先返回。然后hitTest遇到下面幾種情況也不會觸發(fā):

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

如下圖:

圖1.1.jpg

添加順序?yàn)锳 - B - C - D - E社付。按照官方說法承疲,可以得出結(jié)論是:
無論點(diǎn)擊A還是B都會先去查找B視圖是否能夠響應(yīng)點(diǎn)擊(包含該點(diǎn)),因?yàn)锽在視圖樹中比A后添加鸥咖。
我們可以看下調(diào)用棧順序。
先點(diǎn)擊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

先點(diǎn)擊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)鏈和手勢的先后順序是什么

我們可以再點(diǎn)擊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會按照視圖樹去查找,始終查找的是最遠(yuǎn)的那個View.查找順序如下圖:

image.png

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

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

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

我們先點(diǎ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的最遠(yuǎn)端subview開始(即B),接著再是B視圖的最遠(yuǎn)端
(即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,再次點(diǎn)擊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)點(diǎn)擊事件的煞躬。
那么這時候我們就要重寫父視圖的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有重疊部分,我們希望的是“點(diǎn)擊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)系作者
  • 序言:七十年代末娜汁,一起剝皮案震驚了整個濱河市兄朋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌傅事,老刑警劉巖融虽,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異有额,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)茴迁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門萤衰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人脆栋,你說我怎么就攤上這事∨绿牛” “怎么了秦踪?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵掸茅,是天一觀的道長柠逞。 經(jīng)常有香客問我,道長板壮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任慕购,我火速辦了婚禮茬底,結(jié)果婚禮上获洲,老公的妹妹穿的比我還像新娘。我一直安慰自己贡珊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布爱致。 她就那樣靜靜地躺著寒随,像睡著了一般。 火紅的嫁衣襯著肌膚如雪妻往。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天纫普,我揣著相機(jī)與錄音好渠,去河邊找鬼。 笑死拳锚,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的但指。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拦坠,長吁一口氣:“原來是場噩夢啊……” “哼剩岳!你這毒婦竟也來了贞滨?” 一聲冷哼從身側(cè)響起拍棕,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤绰播,失蹤者是張志新(化名)和其女友劉穎骄噪,沒想到半個月后蠢箩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡滔韵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年掌实,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宴卖。...
    茶點(diǎn)故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡忱嘹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拘悦,到底是詐尸還是另有隱情,我是刑警寧澤分苇,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布屁桑,位于F島的核電站,受9級特大地震影響蘑斧,放射性物質(zhì)發(fā)生泄漏须眷。R本人自食惡果不足惜沟突,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扩劝。 院中可真熱鬧职辅,春花似錦棒呛、人聲如沸域携。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽气筋。三九已至旋圆,卻和暖如春宠默,著一層夾襖步出監(jiān)牢的瞬間灵巧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工瓤球, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留敏弃,地道東北人卦羡。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓绿饵,卻偏偏與公主長得像瓶颠,于是被迫代替她去往敵國和親拟赊。 傳聞我的和親對象是個殘疾皇子粹淋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評論 2 354