Hit-Testing in iOS

Hit-testing翻译为中文是"命中测试",是确定touch-point是否在一个View内的过程,最终命中的View被称为hit-test view。iOS使用hit-testing来确定那一个UIView是用户手指下最靠前的且能够接收touch事件的view。hit-testing通过反向前序深度优先算法来便利视图的层次结构,从而实现上述功能。

在解释hit-testing 如何工作前,我们先要了解下hit-testing是在什么时候执行的。以下图分解了一个手指从触摸屏幕到离开屏幕的过程。

hit-test-touch-event-flow.png

如上图所示,每次手指触碰屏幕都会触发hit-testing, 并且hit-testing在各个视图和gesture recognizer接收UIEvent 事件之前触发。

注意:不清楚是什么原因,hit-testing会连续执行多次。并且,hit-test view 的hit-testing也会执行多次。

hit-testing完成后,最前方且能够接收事件的view就会被确定为hit-test view。hit-test view与各个阶段(begin,move,end,cancel)的触摸事件序列的UITouch对象相关联。hit-test view确定后,开始接收touch事件序列。

注意:需要了解的是,手指被移动到了hit-test view外,而移动到了另一个view内时,hit-test视图依旧会接收整个touch事件的序列。

“The touch object is associated with its hit-test view for its lifetime, even if the touch later moves outside the view.”

Event Handling Guide for iOS, iOS Developer Library

如前所述,命中测试采用反向预定深度优先遍历(先访问根结点,然后遍历其子树由高到低的指标)。这种遍历可以减少遍历迭代次数,并在搜索到第一个包含touch-point的最深的子视图中停止搜索过程。这可能是因为一个视图总是在它的父视图之前渲染,而兄弟视图总是比在subviews具有较低索引的兄弟视图先渲染。这样,多个重叠的视图都包含一个touch-point时,在右子树的最深的视图是第一个被渲染的。

“Visually, the content of a subview obscures all or part of the content of its parent view. Each superview stores its subviews in an ordered array and the order in that array also affects the visibility of each subview. If two sibling subviews overlap each other, the one that was added last (or was moved to the end of the subview array) appears on top of the other.”

View Programming Guide for iOS, iOS Developer Library

下图显示了一个视图层次树及其匹配的用户界面。从左到右树枝序列影响着subviews的数组顺序。

hit-test-view-hierarchy.png

图中可以看到,“VIewA”和“ViewB”和它们的子视图,“ViewA.2”和“View B.1”有重叠。但是“ViewB”在subViews中的索引值高于“ViewA”,“ViewB”和它的子视图会展现在“View A”和它的子视图之上。 因此,用户手指触摸 “View.b.1”与”View.A.2”重叠区域时,hit-testing返回“View.B.1”。

通过应用深度优先反向前序遍历算法,各个视图的遍历路径如图:

hit-test-depth-first-traversal.png

遍历算法先发送hitTest:withEvent: 到UIWindow,UIWindow是视图层次结构的根视图。此方法返回的值是包含了触摸点的最靠前面的视图。

下面的流程图说明了命中测试逻辑。

hit-test-flowchart.png

以下代码是hitTest:withEvent:可能的实现:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

hitTest:withEvent:方法首先检查视图是否允许接收touch事件。如果一个View允许接收touch事件,则必须满足以下所有条件:

  1. self.hidden == NO
  2. self. self.userInteractionEnabled = YES
  3. self.alpha > 0.1
  4. pointInside:withEvent: == YES

当视图View允许接收touch事件时,该方法就会反向枚举它的子视图,然后逐个发送hitTest:withEvent:消息,直到有一个子视图返回了nil。第一个返回非nil值的子视图就是这些子视图中最靠前且在touch-point下的视图。如果所有视图都返回nil或者该视图没有子视图则函数返回self.

如果视图View不允许接收touch事件,该方法返回nil,而不需要在反向枚举。因此,hit-testing并不需要访问视图层次树上的所有视图。

hitTest:withEvent:运用场景

hitTest:withEvent:可以被覆盖,当所有触摸事件阶段的所有阶段的触摸事件想要被一个视图处理重定向到另外一个视图。

**注意: **因为hit-test仅仅在触摸事件顺序的第一次触摸事件(UITouchPhaseBegan phase(阶段)的触摸事件)发送给他的接收者之前,覆盖hitTest:withEvent:来重定向事件将会重定向所有phase的触摸事件。

1.增加视图区域

覆盖hitTest:withEvent:方法的一个用途就是,当一个视图的触摸区域应该大于他的边界的时候。例如下面的插图显示了一个大小为20*20的视图。这个大小对于处理附近的触摸来说太小了。因此,他的触摸区域可以通过覆盖hitTest:withEvent:在每个方向增加10。

hit-test-increase-touch-area.png
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    CGRect touchRect = CGRectInset(self.bounds, -10, -10);
    if (CGRectContainsPoint(touchRect, point)) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

注意:为了能够正确的调用hit-test,父视图的边界应该包含子视图希望触摸的区域,或者他的hitTest:withEvent:方法也应该被覆盖来包含期望的触摸区域。

2.传递触摸事件给下面的视图

有的时候对于一个视图忽略触摸事件并传递给下面的视图是很重要的。例如,假设一个透明的视图覆盖在应用内所有视图的最上面。覆盖层有子视图应该相应触摸事件的一些控件和按钮。但是触摸覆盖层的其他区域应该传递给覆盖层下面的视图。为了完成这个行为,覆盖层需要覆盖hitTest:withEvent:方法来返回包含触摸点的子视图中的一个,然后其他情况返回nil,包括覆盖层包含触摸点的情况:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *hitTestView = [super hitTest:point withEvent:event];
    if (hitTestView == self) {
        hitTestView = nil;
    }
    return hitTestView;
}

3.传递触摸事件给子视图
一个不同的使用场景可能需要父视图重定向所有的触摸事件给他唯一的子视图。这个行为是有必要的当子视图部分占据他的父视图,但是子视图应该响应所有的触摸事件包括发生在父视图上的。例如,假设一个由一个父视图和一个pagingEnabled设置为YES和clipsToBounds设置为NO(为了实现传动带的效果)的UIScrollView组成的图片浏览器:

hit-test-pass-touches-to-subviews.png

为了使UIScrollView响应不发生在自己边界内但是在父视图的边界内的触摸事件,父视图的hitTest:withEvent:方法应该像下面这样重写:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *hitTestView = [super hitTest:point withEvent:event];
    if (hitTestView) {
        hitTestView = self.scrollView;
    }
    return hitTestView;
}

如有问题请指正, 谢谢。

另外一篇是一位大神的翻译,翻译了一半的时候才发现的,索性后半部分就直接粘贴了。

大神翻译:http://joywii.github.io/blog/2015/03/17/ioszhong-de-hit-testing/

原文地址:http://smnh.me/hit-testing-in-ios/

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

推荐阅读更多精彩内容

  • 好奇触摸事件是如何从屏幕转移到APP内的?困惑于Cell怎么突然不能点击了?纠结于如何实现这个奇葩响应需求?亦或是...
    Lotheve阅读 56,852评论 51 598
  • 在iOS开发中经常会涉及到触摸事件。本想自己总结一下,但是遇到了这篇文章,感觉总结的已经很到位,特此转载。作者:L...
    WQ_UESTC阅读 5,995评论 4 26
  • 用户以多种方式操纵他们的iOS设备,例如触摸屏幕或摇动设备。 iOS会解释用户何时以及如何操作硬件并将此信息传递到...
    坤坤同学阅读 3,984评论 7 19
  • 一个触摸点(如touch-point)是否和一个绘制在屏幕上的图像对象(如UIView)相交(intersects...
    Mrshang110阅读 3,331评论 2 21
  • 一.hitTest:withEvent:调用过程 iOS系统检测到手指触摸(Touch)操作时会将其放入当前活动A...
    随风飘荡的小逗逼阅读 2,310评论 0 3