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.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_; layer = >
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_; layer = >
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_; layer = >
2020-11-29 12:38:08.563885+0800 demo22[64342:1317416] AView_hitTest_end_; layer = >
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_; layer = >
2020-11-29 11:50:10.134820+0800 demo22[37861:1197365] BView_hitTest_end_; layer = >
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_; layer = >
2020-11-29 12:05:18.056852+0800 demo22[48256:1243168] BView_hitTest_end_; layer = >
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;
}

你可能感兴趣的:(hitTest由浅入深)