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


145

尽管大多数Apple文档都写得很好,但我认为“ iOS事件处理指南 ”是一个例外。我很难清楚地了解那里的描述。

该文件说,

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

难道就好像系统只hitTest:withEvent:调用了最顶层的视图,即调用pointInside:withEvent:了所有子视图,并且如果特定子视图的返回值为YES,则pointInside:withEvent:该子视图的子类的调用一样?


3
一个很好的教程,帮助我实现了链接
anneblue 2015年

因为这可能相当于新文档现在是developer.apple.com/documentation/uikit/uiview/1622469-hittest
心教堂

Answers:


173

似乎是一个基本的问题。但我同意您的要求,该文档不如其他文档那么清晰,所以这是我的答案。

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

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

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

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


您是说hitTest:withEvent:所有子视图最终都会执行吗?
realstuff02 2011年

2
是。只需hitTest:withEvent:在您的视图中覆盖(pointInside如果需要),就可以打印日志并进行调用,[super hitTest...以找出hitTest:withEvent:调用顺序是谁。
MHC

不应该在第3步中提到“如果返回结果为YES,它将发送hitTest:withEvent:...不应该是pointInside:withEvent吗?我认为它会向所有子视图发送
pointInside

早在2月,它首先发送了hitTest:withEvent :,其中将pointInside:withEvent:发送给它自己。我没有使用以下SDK版本重新检查此行为,但我认为发送hitTest:withEvent:更有意义,因为它提供了事件是否属于视图的更高级别的控制;pointInside:withEvent:告诉事件位置是否在视图上,而不是事件是否属于视图。例如,子视图可能不希望处理事件,即使其位置在子视图上。
MHC

1
WWDC2014会议235-高级滚动视图和触摸处理技术为该问题提供了很好的解释和示例。
antonio081014 2015年

297

我认为您在将子类与视图层次结构混淆。该文档说的如下。假设您具有此视图层次结构。对于层次结构,我不是在谈论类层次结构,而是在视图层次结构中的视图,如下所示:

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

假设您将手指放在里面D。将会发生以下情况:

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

谢谢你的回答。您所描述的也是我的想法,但是@MHC谈到hitTest:withEvent:B,C和D时也会被调用。如果D是C的子视图而不是A的子视图,该怎么办?我想我感到困惑...
realstuff02 '02

2
在我的图形中,D是C的子视图
。– pgb

1
不会A返回YES为好,就像CD呢?
Martin Wickman

2
不要忘记,不可见的视图(通过.hidden或不透明度低于0.1)或关闭用户交互将永远不会响应hitTest。我不认为首先要在这些对象上调用hitTest。
强尼

只是想添加hitTest:withEvent:可以在所有视图上调用,这取决于它们的层次结构。
Adithya

47

我发现iOS中的这项“ 命中测试”非常有帮助

在此处输入图片说明

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

因此,您需要将此添加到UIView的子类中,并让层次结构中的所有视图都继承自它?
Guig

21

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

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

假设X-用户的触摸。pointInside:withEvent:关于B回报NO,所以hitTest:withEvent:回报AUIView当您需要在最顶部可见的视图上触摸时,我在上面写了类别来处理问题。

- (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. 如果内部有触摸selfself将被视为潜在结果。
  3. 递归检查所有子视图是否命中。如果有,将其退回。
  4. 否则返回self或nil,具体取决于步骤2的结果。

注意,[self.subviewsreverseObjectEnumerator]需要按照从最上到下的视图层次结构进行操作。并检查clipsToBounds以确保不测试蒙版的子视图。

用法:

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

苹果官方指南也提供了一些很好的例证。

希望这对某人有帮助。


惊人!感谢清晰的逻辑和出色的代码片段,解决了我的头大难题!
汤普森2014年

@狮子,好的答案。您也可以在第一步检查是否相等以清除颜色。
aquarium_moose

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

我发现这是最容易理解的答案,它与我对实际行为的观察非常接近。唯一的区别是子视图以相反的顺序枚举,因此靠近前视图的子视图优先于其后的兄弟姐妹接收触摸。
道格拉斯·希尔

@DouglasHill感谢您的纠正。最好的问候
河马

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

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()看起来像

hitTest() {

    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]

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.