ChatGPT解决这个技术问题 Extra ChatGPT

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

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

文件说,

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

是不是只有最顶层视图的 hitTest:withEvent: 被系统调用,它调用所有子视图的 pointInside:withEvent:,如果特定子视图的返回为 YES,则调用该子视图的 pointInside:withEvent:子类?

一个非常好的教程,帮助了我link
与此等效的较新文档现在可能是 developer.apple.com/documentation/uikit/uiview/1622469-hittest

p
pgb

我认为您将子类化与视图层次结构混淆了。医生说的内容如下。假设你有这个视图层次结构。通过层次结构我不是在谈论类层次结构,而是在视图层次结构中的视图,如下所示:

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

假设您将手指放在 D 内。以下是将会发生的事情:

hitTest:withEvent: 在视图层次结构的最顶层视图 A 上调用。 pointInside:withEvent: 在每个视图上递归调用。 pointInside:withEvent: 在 A 上调用,返回 YES pointInside:withEvent: 在 B 上调用,返回 NO pointInside:withEvent: 在 C 上调用,返回 YES pointInside:withEvent: 在 D 上调用,返回 YES返回 YES 的视图,它将向下查看层次结构以查看发生触摸的子视图。在这种情况下,从 A、C 和 D 中,将是 D。D 将是命中测试视图


谢谢你的回答。您所描述的也是我的想法,但@MHC 说 B、C 和 D 的 hitTest:withEvent: 也被调用。如果 D 是 C 的子视图,而不是 A,会发生什么?我觉得我糊涂了……
在我的图中,D 是 C 的子视图。
A 不会像 CD 一样返回 YES 吗?
不要忘记不可见的视图(通过 .hidden 或低于 0.1 的不透明度)或关闭用户交互的视图将永远不会响应 hitTest。我认为首先没有在这些对象上调用 hitTest。
只是想添加 hitTest:withEvent: 可以在所有视图上调用,具体取决于它们的层次结构。
M
MHC

这似乎是一个非常基本的问题。但我同意你的观点,该文件不像其他文件那样清晰,所以这是我的回答。

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

它调用 pointInside:withEvent: of self

如果返回为 NO,hitTest:withEvent: 返回 nil。故事的结局。

如果返回是 YES,它会发送 hitTest:withEvent: 消息到它的子视图。它从顶级子视图开始,并继续到其他视图,直到子视图返回非零对象,或者所有子视图都收到消息。

如果子视图第一次返回非零对象,则第一个 hitTest:withEvent: 返回该对象。故事的结局。

如果没有子视图返回非 nil 对象,则第一个 hitTest:withEvent: 返回 self

这个过程递归地重复,所以通常最终返回视图层次结构的叶视图。

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


您的意思是所有子视图中的 hitTest:withEvent: 最终都会执行吗?
是的。只需在您的视图中覆盖 hitTest:withEvent:(如果需要,还可以覆盖 pointInside),打印日志并调用 [super hitTest... 以找出谁的 hitTest:withEvent: 以何种顺序被调用。
不应该在第 3 步中提到“如果返回是 YES,它会发送 hitTest:withEvent: ...不应该是 pointInside:withEvent 吗?我认为它会将 pointInside 发送到所有子视图?
早在 2 月,它首先发送了 hitTest:withEvent:,其中一个 pointInside:withEvent: 被发送给它自己。我没有使用以下 SDK 版本重新检查此行为,但我认为发送 hitTest:withEvent: 更有意义,因为它提供了对事件是否属于视图的更高级别的控制; pointInside:withEvent: 告诉事件位置是否在视图上,而不是事件是否属于视图。例如,子视图可能不想处理事件,即使它的位置在子视图上。
WWDC2014 Session 235 - Advanced Scrollviews and Touch handling Techniques 为这个问题提供了很好的解释和示例。
S
Samps

我觉得这个Hit-Testing in iOS很有帮助

https://i.stack.imgur.com/FL27E.png

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

编辑斯威夫特 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
}

所以你需要把它添加到 UIView 的子类中,并让你的层次结构中的所有视图都继承自它?
L
Lion

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

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

假设 X - 用户的触摸。 B 上的 pointInside:withEvent: 返回 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 设置为 NO 的视图;如果触摸在自我内部,自我将被视为潜在结果。递归检查所有子视图是否命中。如果有,请退回。否则根据步骤 2 的结果返回 self 或 nil。

请注意,[self.subviewsreverseObjectEnumerator] 需要从上到下遵循视图层次结构。并检查 clipsToBounds 以确保不测试被屏蔽的子视图。

用法:

在子类视图中导入类别。用这个替换 hitTest:withEvent:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

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

希望这可以帮助某人。


惊人!感谢清晰的逻辑和出色的代码片段,解决了我的难题!
@Lion,很好的答案。您也可以在第一步检查相等性以清除颜色。
h
hippo

它显示像这个片段!

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

我发现这是最容易理解的答案,它与我对实际行为的观察非常吻合。唯一的区别是子视图以相反的顺序枚举,因此靠近前面的子视图优先接收触摸而不是它们后面的兄弟。
@DouglasHill 感谢您的更正。此致
y
yoAlex5

iOS 触控

1. User touch
2. event is created
3. hit testing by coordinates - find first responder - UIView and successors (UIWindow) 
 3.1 hit testing - recursive find the most deep view
  3.1.1 point inside - check coordinates
4. Send Touch Event to the First Responder

类图

命中测试

查找First Responder

在这种情况下,First Responder 是最深的 UIView point()hitTest() 在内部使用 point())方法,该方法返回 true。它总是通过 UIApplication -> UIWindow -> First Responder

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

}

向第一响应者发送触摸事件

//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?)

我们来看一个例子

https://i.stack.imgur.com/FvbjU.png

响应者链

它是一种 chain of responsibility 模式。它由可以处理 UIEventUIResponser 组成。在这种情况下,它从覆盖 touch... 的第一响应者开始。 super.touch... 调用响应者链中的下一个链接

Responder chain 也被事件总线等 addTargetsendAction 方法使用

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

*在处理多点触控时会考虑isExclusiveTouch

[Android onTouch]


m
mortadelo

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