【问题标题】:Capturing touches on a subview outside the frame of its superview using hitTest:withEvent:使用 hitTest:withEvent 捕获其父视图框架之外的子视图的触摸:
【发布时间】:2012-07-31 00:39:57
【问题描述】:

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

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

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

示例:

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

图表:

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

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

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

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

谢谢!

【问题讨论】:

    标签: objective-c ios uiview-hierarchy


    【解决方案1】:

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

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

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

    【讨论】:

    • 这看起来不错,感谢您这样做。但是,有一个问题:你为什么要做 return [super hitTest:point withEvent:event]; ?如果没有触发触摸的子视图,您不会只返回 nil 吗?苹果说如果没有子视图包含触摸,hitTest 返回 nil。
    • 嗯...是的,这听起来差不多。此外,为了正确的行为,对象需要以相反的顺序迭代(因为最后一个是视觉上最顶层的)。编辑代码以匹配。
    • 我刚刚使用您的解决方案使 UIButton 捕获触摸,它位于 UIView 内部,它位于(显然)UICollectionView 内部的 UICollectionViewCell 内部。我必须继承 UICollectionView 和 UICollectionViewCell 来覆盖这三个类的 hitTest:withEvent:。它就像魅力一样!谢谢!!
    • 根据所需的用途,它应该返回 [super hitTest:point withEvent:event] 或 nil。返回 self 将导致它接收所有内容。
    • 这是来自 Apple 的关于相同技术的问答技术文档:developer.apple.com/library/ios/qa/qa2013/qa1812.html
    【解决方案2】:

    好的,我做了一些挖掘和测试,这就是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];
    }
    

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

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

    【讨论】:

    • 谢谢,这对我有用,尽管我必须做一些更多的逻辑来确定哪些子视图应该实际接收命中。顺便说一句,我从您的示例中删除了一个不需要的中断。
    • 当子视图框架超出父视图框架时,点击事件没有响应!您的解决方案解决了这个问题。非常感谢:-)
    • @toblerpwn(有趣的昵称 :) )您应该编辑这个优秀的旧答案,以使其非常清楚(使用大号粗体大写字母)在哪个类中您必须添加它。干杯!
    【解决方案3】:

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

    【讨论】:

    • 只有在“行为不端”视图的 direct 超级视图上应用覆盖时才有效
    • 我认为这也应该包括对isUserInteractionEnabled的测试。
    【解决方案4】:

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

    【讨论】:

    • 我也想过这个解决方法——这意味着我将不得不复制一些放置逻辑(或重构一些严肃的代码!),但它最终可能确实是我的最佳选择..跨度>
    【解决方案5】:

    如果您的父视图中有许多其他子视图,那么如果您使用上述解决方案,可能大多数其他交互式视图都无法工作,在这种情况下,您可以使用类似这样的东西(在 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)
        }
    }
    

    【讨论】:

      【解决方案6】:

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

      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
      }
      

      【讨论】:

        【解决方案7】:

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

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

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

        【讨论】:

        • 您的博客已被删除,请问在哪里可以找到解释?
        【解决方案8】:

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

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

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

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

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

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

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-02-15
          • 2014-10-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多