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

时间:2011-02-10 18:57:48

标签: ios uiview uikit

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

该文件说,

  

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

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

7 个答案:

答案 0 :(得分:291)

我认为您对视图层次结构的子类化感到困惑。该文件所说的内容如下。假设您有此视图层次结构。按层次结构,我不是在讨论类层次结构,而是在视图层次结构中查看,如下所示:

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

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

  1. hitTest:withEvent:A上调用,pointInside:withEvent:是视图层次结构的最顶层视图。
  2. 在每个视图上递归调用
  3. pointInside:withEvent:
      A上调用
    1. YES,并返回pointInside:withEvent:
    2. B上调用
    3. NO,并返回pointInside:withEvent:
    4. C上调用
    5. YES,并返回pointInside:withEvent:
    6. D上调用
    7. YES,并返回YES
  4. 在返回A的视图中,它会向下查看层次结构,以查看触摸发生的子视图。在这种情况下,来自CDD,它将是D
  5. {{1}}将成为热门测试视图

答案 1 :(得分:166)

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

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

  • 它会调用pointInside:withEvent:
  • self
  • 如果返回“否”,则hitTest:withEvent:返回nil。故事的结尾。
  • 如果返回“是”,则会向其子视图发送hitTest:withEvent:条消息。 它从顶级子视图开始,并继续到其他视图,直到子视图 返回非nil对象,或者所有子视图都会收到消息。
  • 如果子视图第一次返回非nil对象,则第一个hitTest:withEvent:将返回该对象。故事的结尾。
  • 如果没有子视图返回非nil对象,则第一个hitTest:withEvent:会返回self

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

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

答案 2 :(得分:43)

我觉得这个Hit-Testing in iOS非常有用

enter image description here

- (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;
}

编辑Swift 4:

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
}

答案 3 :(得分:21)

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

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

假设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;
}
  1. 我们不应发送隐藏或透明视图的触摸事件,也不应将userInteractionEnabled设置为NO的视图发送;
  2. 如果触摸位于self内,则self将被视为潜在结果。
  3. 以递归方式检查所有匹配的子视图。如果有,请退货。
  4. 根据步骤2的结果,返回self或nil。
  5. 注意,[self.subviewsreverseObjectEnumerator]需要遵循从最顶部到底部的视图层次结构。并检查clipsToBounds以确保不测试屏蔽的子视图。

    用法:

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

      Official Apple's Guide也提供了一些很好的插图。

      希望这有助于某人。

答案 4 :(得分:3)

它显示像这个片段!

- (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;
}

答案 5 :(得分:1)

@lion的片段就像​​一个魅力。我将它移植到swift 2.1并将其用作UIView的扩展。如果有人需要,我会在这里发帖。

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
    }
}

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

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

答案 6 :(得分:0)

类图

命中测试

找到一个First Responder

First Responder在这种情况下是最深的UIView point()方法,该方法返回true

func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
func point(inside point: CGPoint, with event: UIEvent?) -> Bool

内部hitTest()看起来像

func hitTest() -> View? {

    if (isUserInteractionEnabled == false || isHidden == true || alpha == 0 || point() == false) { return nil }

    for subview in subviews {
        if subview.hitTest() != nil {
            return subview
        }
    }
        
    return nil

}

将触摸事件发送到First Responder

//UIApplication.shared.sendEvent()

//UIApplication, UIWindow
func sendEvent(_ event: UIEvent)

//UIResponder
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)

我们来看一个例子

响应者链

//UIApplication.shared.sendAction()
func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool

看看例子

class AppDelegate: UIResponder, UIApplicationDelegate {
    @objc
    func foo() {
        //this method is called using Responder Chain
        print("foo") //foo
    }
}

class ViewController: UIViewController {
    func send() {
        UIApplication.shared.sendAction(#selector(AppDelegate.foo), to: nil, from: view1, for: nil)
    }
}

[Android onTouch]