iOS的事件处理 - hitTest:withEvent:和pointInside:withEvent:如何相关?

问题描述 投票:139回答:6

虽然大多数苹果文档编写得很好,但我认为'Event Handling Guide for iOS'是一个例外。我很难清楚地了解那里描述的内容。

该文件说,

在命中测试中,窗口在视图层次结构的最顶层视图上调用hitTest:withEvent:;此方法通过在视图层次结构中返回YES的每个视图上递归调用pointInside:withEvent:来进行,从层次结构向下继续,直到找到触摸发生在其边界内的子视图。该视图成为热门测试视图。

因此,只有系统调用最顶层视图的hitTest:withEvent:,调用所有子视图的pointInside:withEvent:,如果从特定子视图返回YES,则调用该子视图的子类的pointInside:withEvent:

ios uiview uikit
6个回答
167
投票

这似乎是一个非常基本的问题。但我同意你的意见,文件不像其他文件那么清楚,所以这是我的答案。

在UIResponder中实施hitTest:withEvent:执行以下操作:

  • 它叫pointInside:withEvent:self
  • 如果回报为NO,则hitTest:withEvent:返回nil。故事的结尾。
  • 如果返回为YES,则将hitTest:withEvent:消息发送到其子视图。它从顶级子视图开始,并继续到其他视图,直到子视图返回非nil对象,或者所有子视图都接收到该消息。
  • 如果子视图第一次返回非nil对象,则第一个hitTest:withEvent:返回该对象。故事的结尾。
  • 如果没有子视图返回非nil对象,则第一个hitTest:withEvent:返回self

此过程以递归方式重复,因此通常最终返回视图层次结构的叶视图。

但是,您可以覆盖hitTest:withEvent以执行不同的操作。在许多情况下,覆盖pointInside:withEvent:更简单,仍然提供了足够的选项来调整应用程序中的事件处理。


292
投票

我认为你混淆了子视图与视图层次结构。该文件所说的内容如下。假设您有此视图层次结构。按层次结构,我不是在讨论类层次结构,而是在视图层次结构中查看,如下所示:

+----------------------------+
|A                           |
|+--------+   +------------+ |
||B       |   |C           | |
||        |   |+----------+| |
|+--------+   ||D         || |
|             |+----------+| |
|             +------------+ |
+----------------------------+

假设你把手指放在D里面。这是将要发生的事情:

  1. hitTest:withEvent:A上调用,pointInside:withEvent:是视图层次结构的最顶层视图。
  2. pointInside:withEvent:在每个视图上递归调用。 AYES被召唤,并返回pointInside:withEvent: BNO被召唤,并返回pointInside:withEvent: CYES被召唤,并返回pointInside:withEvent: DYES被召唤,并返回YES
  3. 在返回A的视图中,它将向下查看层次结构以查看触摸发生的子视图。在这种情况下,从CDD,它将是D
  4. Hit-Testing in iOS将成为热门测试视图

44
投票

我发现这个- (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; } 非常有帮助

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    if self.point(inside: point, with: event) {
        return super.hitTest(point, with: event)
    }
    guard isUserInteractionEnabled, !isHidden, alpha > 0 else {
        return nil
    }

    for subview in subviews.reversed() {
        let convertedPoint = subview.convert(point, from: self)
        if let hitView = subview.hitTest(convertedPoint, with: event) {
            return hitView
        }
    }
    return nil
}

编辑Swift 4:

+----------------------------+
|A +--------+                |
|  |B  +------------------+  |
|  |   |C            X    |  |
|  |   +------------------+  |
|  |        |                |
|  +--------+                | 
|                            |
+----------------------------+

21
投票

感谢您的回答,他们帮我解决了“叠加”视图的情况。

X

假设pointInside:withEvent: - 用户的触摸。在B上的NO返回hitTest:withEvent:,所以A返回UIView。我在- (UIView *)overlapHitTest:(CGPoint)point withEvent:(UIEvent *)event { // 1 if (!self.userInteractionEnabled || [self isHidden] || self.alpha == 0) return nil; // 2 UIView *hitView = self; if (![self pointInside:point withEvent:event]) { if (self.clipsToBounds) return nil; else hitView = nil; } // 3 for (UIView *subview in [self.subviewsreverseObjectEnumerator]) { CGPoint insideSubview = [self convertPoint:point toView:subview]; UIView *sview = [subview overlapHitTest:insideSubview withEvent:event]; if (sview) return sview; } // 4 return hitView; } 上写了类别来处理问题,当你需要在最顶级的可见视图上接触时。

userInteractionEnabled
  1. 我们不应该为隐藏或透明的视图或NO设置为self的视图发送触摸事件;
  2. 如果触摸在self内,[self.subviewsreverseObjectEnumerator]将被视为潜在结果。
  3. 以递归方式检查所有子视图是否为hit。如果有,请退货。
  4. 否则返回self或nil,具体取决于步骤2的结果。

请注意,clipsToBounds需要遵循从最顶部到底部的视图层次结构。并检查hitTest:withEvent:以确保不测试蒙面子视图。

用法:

  1. 在子类视图中导入类别。
  2. 用这个替换- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { return [self overlapHitTest:point withEvent:event]; }
Official Apple's Guide

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01) { return nil; } if (![self pointInside:point withEvent:event]) { return nil; } __block UIView *hitView = self; [self.subViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { CGPoint thePoint = [self convertPoint:point toView:obj]; UIView *theSubHitView = [obj hitTest:thePoint withEvent:event]; if (theSubHitView != nil) { hitView = theSubHitView; *stop = YES; } }]; return hitView; } 也提供了一些很好的插图。

希望这有助于某人。


3
投票

它显示像这个片段!

extension UIView {
    func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // 1
        if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
            return nil
        }
        //2
        var hitView: UIView? = self
        if !self.pointInside(point, withEvent: event) {
            if self.clipsToBounds {
                return nil
            } else {
                hitView = nil
            }
        }
        //3
        for subview in self.subviews.reverse() {
            let insideSubview = self.convertPoint(point, toView: subview)
            if let sview = subview.overlapHitTest(insideSubview, withEvent: event) {
                return sview
            }
        }
        return hitView
    }
}

1
投票

@lion的片段就像​​一个魅力。我将它移植到swift 2.1并将其用作UIView的扩展。我在这里张贴以防有人需要它。

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    let uiview = super.hitTest(point, withEvent: event)
    print("hittest",uiview)
    return overlapHitTest(point, withEvent: event)
}

要使用它,只需在您的uiview中覆盖hitTest:point:withEvent,如下所示:

qazxswpoi
© www.soinside.com 2019 - 2024. All rights reserved.