98

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

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

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

例子:

  • (E) is EditView,所有视图的父级
  • (M) is MenuView, EditView 的一个子视图
  • (B) 是ButtonView,MenuView 的子视图

图表:

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

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

目标:我认为覆盖hitTest:withEvent:可以解决这个问题,但我不明白如何。就我而言,应该在(我的“主”超级视图)hitTest:withEvent:中被覆盖?EditView还是应该在MenuView未接收触摸的按钮的直接超级视图中覆盖它?还是我想错了?

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

谢谢!

4

8 回答 8

152

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

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

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

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

于 2013-02-14T13:13:11.897 回答
33

好的,我做了一些挖掘和测试,这就是hitTest:withEvent工作原理 - 至少在高水平上。想象这个场景:

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

图表:

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

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

但是,如果您hitTest:withEvent:在 (M) 中实现,应用程序中任何地方的点击都将被发送到 (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];
}

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

我希望这对以后遇到这个问题的其他人有所帮助。:)

于 2012-08-03T17:39:13.773 回答
30

在斯威夫特 5

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
}
于 2015-03-28T14:24:54.277 回答
2

我要做的是让 ButtonView 和 MenuView 都存在于视图层次结构中的同一级别,方法是将它们都放在一个框架完全适合它们的容器中。这样,被剪切项目的交互区域将不会因为它的超级视图边界而被忽略。

于 2012-08-02T04:07:01.913 回答
1

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

class BoundingSubviewsViewExtension: UIView {

    @IBOutlet var targetView: UIView!

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // Convert the point to the target view's coordinate system.
        // The target view isn't necessarily the immediate subview
        let pointForTargetView: CGPoint? = targetView?.convert(point, from: self)
        if (targetView?.bounds.contains(pointForTargetView!))! {
            // The target view may have its view hierarchy,
            // so call its hitTest method to return the right hit-test view
            return targetView?.hitTest(pointForTargetView ?? CGPoint.zero, with: event)
        }
        return super.hitTest(point, with: event)
    }
}
于 2017-11-03T06:51:21.773 回答
0

将以下代码行放入您的视图层次结构中:

- (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”中有关于“自定义标注:按钮不可点击的问题”的解释。

希望对你有帮助……!!!

于 2015-05-19T10:50:45.783 回答
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
}
于 2016-08-26T17:16:59.213 回答
0

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

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

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

* +并且^是 3 种不同的用户触摸。+在 Child 上无法识别 touch 每当您在其边界的任何位置触摸祖父时,无论您在祖父中触摸的哪个位置,祖父都会调用其所有直接子视图.hitTest。所以在这里,对于 3 次触摸,祖父母会打电话给parent.hitTest()

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

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

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

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

于 2021-01-30T15:22:34.550 回答