hitTest由浅入深

本文将从如下几个方面来介绍它:

  • 什么是hitTest
  • hitTest、响应者链和触摸事件的先后顺序是什么
  • hitTest实现思路以及模仿
  • 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:实现),如果包含就从它的子视图里面查找,按照由远及近的顺序(先查找最后添加的subview),如果事件触发点在该视图里面则优先返回。然后hitTest遇到下面几种情况也不会触发:

  • hidden = YES
  • userInteractionEnabled = NO
  • alpha <= 0.01
  • 父视图clipsToBounds = NO 且子视图超出父视图的bounds所在范围

如下图:

图1.1.jpg

添加顺序为A - B - C - D - E。按照官方说法,可以得出结论是:
无论点击A还是B都会先去查找B视图是否能够响应点击(包含该点),因为B在视图树中比A后添加。
我们可以看下调用栈顺序。
先点击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

通过查看调用栈我们可以发现上述结论的正确性。

2.hitTest、响应链和手势的先后顺序是什么

我们可以再点击C看下调用栈(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内,判断为不在,然后再去查找A是否包含;如果包含,则进一步查找A的子视图C。如果C能够响应,则C开始出发事件响应。即触发UIResponder的touchesBegan事件,然后往视图层级链向上抛出事件。从C -> A -> controller.view - > controller -> window -> UIApplication -> 事件丢弃
所以,hitTest只是来查找能够响应点击事件的View,然后该View触发事件响应,然后沿着视图层级链往上传递。刚好,是沿着相反的方向。
也可以看出,手势是基于UIResponser 的touch事件封装,优先级比touch事件高
总结:

  • hitTest是查找响应者链的方法,顺序是由远及近。(优先查找父视图上最远的子视图)
  • 响应者链当然就是由由远及近。
  • 触摸事件顺序刚好和响应者链相反。

3.hitTest实现思路以及模仿

我们先点击E查看下调用栈:

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的调用栈如下:

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

我们可以对比发现,可官方的调用栈简直是一模模一样样,是不是感觉很神奇呢?

4.hitTest使用场景

  • 场景1

有时候我们会碰到如下情况:


image.png

子视图bounds超出父视图的容器,如果不加处理这时候是无法响应点击事件的。
那么这时候我们就要重写父视图的hitTest方法,把最佳响应视图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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350