本文將從如下幾個方面來介紹它:
- 什么是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所在范圍
如下圖:
添加順序?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.查找順序如下圖:
首先舆声,會找到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
有時候我們會碰到如下情況:
子視圖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;
}