使用hitTest:withEvent捕获其超级视图框架外的子视图的触摸:

时间:2012-08-02 03:41:24

标签: objective-c ios uiview-hierarchy

我的问题:我有一个超级视图EditView,基本上占据整个应用程序框架,子视图MenuView只占底部~20%,并且然后MenuView包含自己的子视图ButtonView,它实际上位于MenuView的边界之外(类似于:ButtonView.frame.origin.y = -100)。

(注意:EditView有其他子视图不属于MenuView的视图层次结构,但可能会影响答案。)

您可能已经知道这个问题:当ButtonViewMenuView的范围内时(或者更具体地说,当我的触摸在MenuView的范围内时){{1}响应触摸事件。当我的触摸超出ButtonView的范围(但仍在MenuView的范围内)时,ButtonView不会收到任何触摸事件。

示例:

  • (E)是ButtonView,是所有观点的父级
  • (M)是EditView,是EditView的子视图
  • (B)是MenuView,是MenuView的子视图

图表:

ButtonView

因为(B)在(M)的框架之外,所以(B)区域中的抽头将永远不会被发送到(M) - 实际上,(M)在这种情况下从不分析触摸,并且触摸被发送到层次结构中的下一个对象。

目标:我认为重写+------------------------------+ |E | | | | | | | | | |+-----+ | ||B | | |+-----+ | |+----------------------------+| ||M || || || |+----------------------------+| +------------------------------+ 可以解决这个问题,但我不知道究竟是怎么回事。在我的情况下,hitTest:withEvent:(我的'主'超级视图)中是否应该覆盖hitTest:withEvent:?或者它应该在EditView覆盖,是否没有接收触摸的按钮的直接超级视图?或者我错误地想到了这个?

如果这需要冗长的解释,一个好的在线资源会有所帮助 - 除了Apple的UIView文档,我还没有说清楚。

谢谢!

7 个答案:

答案 0 :(得分:135)

我已经将接受的答案的代码修改为更通用 - 它处理视图将子视图剪辑到其边界的情况,可能是隐藏的,更重要的是:如果子视图是复杂的视图层次结构,则正确的子视图将是返回。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if (self.clipsToBounds) {
        return nil;
    }

    if (self.hidden) {
        return nil;
    }

    if (self.alpha == 0) {
        return nil;
    }

    for (UIView *subview in self.subviews.reverseObjectEnumerator) {
        CGPoint subPoint = [subview convertPoint:point fromView:self];
        UIView *result = [subview hitTest:subPoint withEvent:event];

        if (result) {
            return result;
        }
    }

    return nil;
}

SWIFT 3

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

    if clipsToBounds || isHidden || alpha == 0 {
        return nil
    }

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

    return nil
}

我希望这有助于任何人尝试将此解决方案用于更复杂的用例。

答案 1 :(得分:30)

好的,我做了一些挖掘和测试,这里是hitTest:withEvent的工作原理 - 至少在高层次上。想象一下这种情况:

  • (E)是EditView ,是所有观看次数的父级
  • (M)是MenuView ,是EditView的子视图
  • (B)是ButtonView ,是MenuView的子视图

图:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

因为(B)在(M)的框架之外,所以(B)区域中的抽头将永远不会被发送到(M) - 实际上,(M)在这种情况下从不分析触摸,并且触摸被发送到层次结构中的下一个对象。

但是,如果在(M)中实现hitTest:withEvent:,则应用程序中任何位置的点按都将被发送到(M)(或者至少它知道它们)。在这种情况下,您可以编写代码来处理触摸并返回应该接收触摸的对象。

更具体地说:hitTest:withEvent:的目标是返回应该接收命中的对象。因此,在(M)中,您可以编写如下代码:

// need this to capture button taps since they are outside of self.frame
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{   
    for (UIView *subview in self.subviews) {
        if (CGRectContainsPoint(subview.frame, point)) {
            return subview;
        }
    }

    // use this to pass the 'touch' onward in case no subviews trigger the touch
    return [super hitTest:point withEvent:event];
}

我对这个方法和这个问题仍然很陌生,所以如果有更有效或正确的方法来编写代码,请发表评论。

我希望以后可以帮助其他人提出这个问题。 :)

答案 2 :(得分:21)

在Swift 4中

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
    for member in subviews.reversed() {
        let subPoint = member.convert(point, from: self)
        guard let result = member.hitTest(subPoint, with: event) else { continue }
        return result
    }
    return nil
}

答案 3 :(得分:0)

如果有人需要,这里是快捷的替代方案

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    if !self.clipsToBounds && !self.hidden && self.alpha > 0 {
        for subview in self.subviews.reverse() {
            let subPoint = subview.convertPoint(point, fromView:self);

            if let result = subview.hitTest(subPoint, withEvent:event) {
                return result;
            }
        }
    }

    return nil
}

答案 4 :(得分:0)

如果您在父视图中有许多其他子视图,那么如果使用上述解决方案,大多数其他交互式视图可能无效,在这种情况下,您可以使用类似的内容(在Swift 3.2中):

bundles.Add(new ScriptBundle("~/bundles/Art")
       .IncludeDirectory("~/scripts/", "*.js", true)

答案 5 :(得分:0)

我花了一些时间来理解这是如何工作的,因为上述解决方案对我不起作用,因为我有很多嵌套的子视图。我将尝试简单地解释一下 hitTest 以便每个人都可以根据情况调整他的代码。

想象一下这种情况: 名为 GrandParent 的视图在其边界内有一个名为 Parent 的子视图。这个 Parent 有一个名为 Child 的子视图,它的边界超出了 Parent 的边界:

-------------------------------------- -> Grand parent 
|              ------- -> Child      |
|              |     |               |
|          ----|-----|--- -> Parent  |
|          |   |   ^ |   |           |
|          |   |     |   |           |
|          ----|-----|---            |
|              | +   |               |
|     *        |-----|               |
|                                    |
--------------------------------------

* +^ 是 3 种不同的用户触摸。儿童无法识别 + 触摸 每当您在其边界的任何位置触摸祖父级时,无论您在祖父级中的何处触摸,祖父级都会调用其所有直接子视图 .hitTest。所以在这里,对于 3 次触摸,祖父母将调用 parent.hitTest()

hitTest 应该返回包含给定点的最远后代(该点是相对于自身边界给出的,在我们的示例中,Grand Parent 使用相对于 Parent 边界的点调用 Parent.hitTest())。

对于 * 触摸,parent.hitTest 返回 nil,这是完美的。对于 ^ 触摸,parent.hitTest 返回 Child,因为它在其边界内(hitTest 的默认实现)。

但是对于 + 触摸,parent.hitTest 默认返回 nil,因为触摸不在父的范围内。因此,我们需要自定义hitTest 实现,以便基本上将相对于Parent 的边界的点转换为相对于Child 边界的点。然后在 child 上调用 hitTest 以查看触摸是否在 child bounds 中(child 应该具有 hitTest 的默认实现,返回在其 bounds 内离它最远的后代,即 : 自身)。

如果您有复杂的嵌套子视图,并且如果上述解决方案对您不起作用,则此说明可能有助于您自定义实现以适应您的视图层次结构。

答案 6 :(得分:-1)

将以下代码行放在视图层次结构中:

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* hitView = [super hitTest:point withEvent:event];
    if (hitView != nil)
    {
        [self.superview bringSubviewToFront:self];
    }
    return hitView;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
    CGRect rect = self.bounds;
    BOOL isInside = CGRectContainsPoint(rect, point);
    if(!isInside)
    {
        for (UIView *view in self.subviews)
        {
            isInside = CGRectContainsPoint(view.frame, point);
            if(isInside)
                break;
        }
    }
    return isInside;
}

为了更清楚,我的博客中解释说:“goaheadwithiphonetech”关于“自定义标注:按钮不可点击的问题”。

我希望能帮到你...... !!!