使用hitTest:withEvent:捕获超出其父视图框架范围的子视图上的触摸事件,涉及IT技术。

102

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

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

您可能已经知道了问题所在:当 ButtonView 处于 MenuView 边界内时(更具体地说,当我的触摸在 MenuView 的边界内时),ButtonView 响应触摸事件。当我的触摸在 MenuView 的边界之外(但仍在 ButtonView 的边界内)时,ButtonView 不会接收到触摸事件。

示例:

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

图示:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+
因为 (B) 区域位于 (M) 的框架之外,所以在该情况下,触摸 (B) 区域的操作不会被发送到 (M),事实上,(M) 没有分析这种情况下的触摸事件,而是将其发送到层级结构中的下一个对象。
目标:我了解到可以通过覆盖 hitTest:withEvent: 来解决此问题,但我不太明白具体如何操作。 在我的情况下,应该在 EditView(我的“主”父视图)中覆盖 hitTest:withEvent: 吗?还是应该在 MenuView 中覆盖它,即未接收到触摸的按钮的直接父视图?或者我对此的想法是错误的?
如果需要详细说明,请提供一个好的在线资源 - 除了苹果的UIView文档,因为这些文档没有让我清楚地了解到这件事。
谢谢!
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;
}

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
}

我希望这能帮助任何想要在更复杂的情况下使用该解决方案的人。


3
我刚刚使用了您的解决方案,使一个UIButton捕获触摸事件,它位于一个UIView内部,而该UIView又位于一个UICollectionViewCell内部,该UICollectionViewCell显然是位于一个UICollectionView中。我必须对UICollectionView和UICollectionViewCell进行子类化,以覆盖这三个类中的hitTest:withEvent:方法。它完美地工作了!谢谢! - Daniel García
不需要在return语句后面加上break语句,因为它永远不会被执行。除此之外,答案非常好。 - datwelk
最后一个 return 应该是 self,不是吗?否则,对视图本身的触摸将被忽略。如果我错了,请纠正我。 - Yevhen Dubinin
3
根据所需用途,它应返回[super hitTest:point withEvent:event]或nil。返回self会导致它接收一切。 - Noam
2
这是苹果关于同一技术的问答技术文档:https://developer.apple.com/library/ios/qa/qa2013/qa1812.html - James Kuang
显示剩余5条评论

33

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

2
只有在“行为异常”的视图的直接父视图上应用覆盖才能使其正常工作。 - Hudi Ilfeld
我认为这也应该包括对 isUserInteractionEnabled 的测试。 - Drew

33

好的,我进行了一些调查和测试,以下是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];
}

我对这种方法和这个问题还很陌生,如果有更有效或正确的编写代码的方法,请留言。

希望能帮助到以后遇到这个问题的其他人。 :)


谢谢,这对我有用。虽然我需要做更多的逻辑来确定哪个子视图应该实际接收命中。顺便说一下,我从你的示例中删除了一个不必要的break语句。 - Daniel Saidi
当子视图的框架超出父视图的框架时,点击事件无响应!您的解决方案解决了这个问题。非常感谢 :-) - byJeevan
@toblerpwn(有趣的昵称 :) ),你应该编辑这个优秀的旧答案,以便非常清楚地(使用大号粗体字母)指出在哪个类中添加它。干杯! - Fattie

2

我的建议是将ButtonView和MenuView放在同一级别的视图层次结构中,通过将它们都放置在一个容器中并使其框架完全适应它们两个来实现。这样被裁剪的项目的交互区域就不会因为其父视图的边界而被忽略。


我也考虑过这个解决方法 - 这意味着我将不得不复制一些放置逻辑(或重构一些严肃的代码!),但最终它可能确实是我的最佳选择。 - toblerpwn

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

0

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

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
}

0

我花了一些时间来理解这个工作原理,因为上面的解决方案对我没有用,因为我有许多嵌套的子视图。我将尝试简单地解释hitTest,以便每个人都可以根据情况调整自己的代码。

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

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

*+^是三种不同的用户触摸。在Child上无法识别+触摸。

当您在grand parent的任何位置触摸它的边界时,grand parent将会调用所有直接子视图的.hitTest,无论您在grand parent的哪个位置触摸。因此,在这里,对于这3个触摸,grand parent将调用parent.hitTest()

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

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

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

如果您有复杂的嵌套子视图,并且以上解决方案对您不起作用,那么这个解释可能对您有用,可以使您的自定义实现适合您的视图层次结构。


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”中详细说明了“自定义标注:按钮无法点击问题”。

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


您的博客已被删除,那么请问我们在哪里可以找到解释? - ishahak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接