【问题标题】:Allowing interaction with a UIView under another UIView允许与另一个 UIView 下的 UIView 交互
【发布时间】:2010-12-14 06:01:46
【问题描述】:

是否有一种简单的方法允许与位于另一个 UIView 下的 UIView 中的按钮进行交互 - 按钮顶部的顶部 UIView 中没有实际对象?

例如,目前我有一个 UIView (A),顶部有一个对象,屏幕底部有一个对象,中间什么都没有。它位于另一个 UIView 的顶部,中间有按钮 (B)。但是,我似乎无法与 B 中间的按钮进行交互。

我可以看到 B 中的按钮 - 我已将 A 的背景设置为 clearColor - 但 B 中的按钮似乎没有收到触摸,尽管实际上在这些按钮之上没有来自 A 的对象。

编辑 - 我仍然希望能够与顶部 UIView 中的对象进行交互

肯定有一种简单的方法可以做到这一点?

【问题讨论】:

  • 这里几乎都解释了:developer.apple.com/iphone/library/documentation/iPhone/… 但基本上,覆盖 hitTest:withEvent:,他们甚至提供了代码示例。
  • 我为此编写了一个小班。 (在答案中添加了一个示例)。那里的解决方案比接受的答案要好一些,因为您仍然可以单击半透明UIView 下的UIButton,而UIView 的非透明部分仍将响应触摸事件。

标签: objective-c ios cocoa-touch


【解决方案1】:

源自 Stuart 的出色且几乎万无一失的答案,以及 Segev 的有用实现,这里有一个 Swift 4 包,您可以将其放入任何项目中:

extension UIColor {
    static func colorOfPoint(point:CGPoint, in view: UIView) -> UIColor {

        var pixel: [CUnsignedChar] = [0, 0, 0, 0]

        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)

        let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)

        context!.translateBy(x: -point.x, y: -point.y)

        view.layer.render(in: context!)

        let red: CGFloat   = CGFloat(pixel[0]) / 255.0
        let green: CGFloat = CGFloat(pixel[1]) / 255.0
        let blue: CGFloat  = CGFloat(pixel[2]) / 255.0
        let alpha: CGFloat = CGFloat(pixel[3]) / 255.0

        let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)

        return color
    }
}

然后用hitTest:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard UIColor.colorOfPoint(point: point, in: self).cgColor.alpha > 0 else { return nil }
    return super.hitTest(point, with: event)
}

【讨论】:

    【解决方案2】:

    基于 HitTest 的解决方案的 Swift 4 实现

    let hitView = super.hitTest(point, with: event)
    if hitView == self { return nil }
    return hitView
    

    【讨论】:

      【解决方案3】:

      斯威夫特 3

      override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
          for subview in subviews {
              if subview.frame.contains(point) {
                  return true
              }
          }
          return false
      }
      

      【讨论】:

        【解决方案4】:

        只是想发布这个,因为我遇到了一些类似的问题,花了大量时间试图在这里实现答案,但没有任何运气。我最终做了什么:

         for(UIGestureRecognizer *recognizer in topView.gestureRecognizers)
         {
             recognizer.delegate=self;
             [bottomView addGestureRecognizer:recognizer];   
         }
         topView.abView.userInteractionEnabled=NO; 
        

        并实施UIGestureRecognizerDelegate

        - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
        {
            return YES;
        }
        

        底部视图是一个带有多个 segue 的导航控制器,我在它上面有一扇门,可以用平移手势关闭。整个事情都嵌入到另一个 VC 中。像魅力一样工作。希望这会有所帮助。

        【讨论】:

          【解决方案5】:

          这是一个 Swift 版本:

          override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
              return !CGRectContainsPoint(buttonView.frame, point)
          }
          

          【讨论】:

            【解决方案6】:

            最近我写了一个课程来帮助我解决这个问题。将其用作UIButtonUIView 的自定义类将传递在透明像素上执行的触摸事件。

            此解决方案比公认的答案要好一些,因为您仍然可以单击半透明 UIView 下的 UIButton,而 UIView 的非透明部分仍将响应触摸事件。

            正如您在 GIF 中看到的,Giraffe 按钮是一个简单的矩形,但透明区域上的触摸事件会传递到下方的黄色 UIButton

            Link to class

            【讨论】:

            • 由于您的代码不是那么长,因此您应该在答案中包含相关的部分,以防您的项目在将来的某个时候被移动或删除。
            • 谢谢,您的解决方案适用于我需要 UIView 的非透明部分来响应触摸事件,而透明部分不需要的情况。太棒了!
            • @Bruce 很高兴它帮助了你!
            【解决方案7】:

            pointInside:withEvent: 的自定义实现确实看起来不错,但处理硬编码坐标对我来说似乎很奇怪。所以我最终使用 CGRectContainsPoint() 函数检查了 CGPoint 是否在按钮 CGRect 内:

            - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
                return (CGRectContainsPoint(disclosureButton.frame, point));
            }
            

            【讨论】:

              【解决方案8】:

              虽然这里的许多答案都有效,但看到这里没有给出最方便、通用和万无一失的答案,我有点惊讶。 @Ash 最接近,除了返回超级视图时发生了一些奇怪的事情......不要那样做。

              此答案取自我对类似问题的回答,here

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

              [super hitTest:point withEvent:event] 将返回该视图层次结构中被触摸的最深视图。如果hitView == self(即如果触摸点下没有子视图),则返回nil,指定此视图应该接收触摸。响应者链的工作方式意味着将继续遍历该点之上的视图层次结构,直到找到将响应触摸的视图。 不要返回父视图,因为它的父视图是否应该接受触摸并不取决于这个视图!

              这个解决方案是:

              • 方便,因为它不需要引用任何其他视图/子视图/对象;
              • 通用,因为它适用于任何纯粹充当可触摸子视图容器的视图,并且子视图的配置不会影响它的工作方式(如果您覆盖 pointInside:withEvent: 以返回特定的可触摸区域)。
              • 万无一失,代码不多……而且这个概念不难理解。

              我经常使用它,因此我将它抽象为一个子类,以便为一个覆盖保存毫无意义的视图子类。作为奖励,添加一个属性以使其可配置:

              @interface ISView : UIView
              @property(nonatomic, assign) BOOL onlyRespondToTouchesInSubviews;
              @end
              
              @implementation ISView
              - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
              {
                  UIView *hitView = [super hitTest:point withEvent:event];
                  if (hitView == self && onlyRespondToTouchesInSubviews) return nil;
                  return hitView;
              }
              @end
              

              然后在任何可能使用普通 UIView 的地方使用此视图。配置它就像将onlyRespondToTouchesInSubviews 设置为YES 一样简单。

              【讨论】:

              • 唯一正确的答案。需要明确的是,如果您想忽略对视图的触摸,而不是对视图中包含的任何按钮(比如说),请按照 Stuart 的说明进行操作。 (我通常称之为“持有人视图”,因为它可以无害地“持有”一些按钮,但是,它不会影响持有人“下方”的任何东西。)
              • 如果您的视图是可滚动的,则使用点的其他一些解决方案会出现问题,例如 UITableView 和 UICollectionView 并且您已经向上或向下滚动。但是,无论滚动如何,此解决方案都有效。
              【解决方案9】:

              这种方法非常干净,并且允许透明子视图也不会对触摸做出反应。只需继承 UIView 并将以下方法添加到其实现中:

              @implementation PassThroughUIView
              
              - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
                  for (UIView *v in self.subviews) {
                      CGPoint localPoint = [v convertPoint:point fromView:self];
                      if (v.alpha > 0.01 && ![v isHidden] && v.userInteractionEnabled && [v pointInside:localPoint withEvent:event])
                          return YES;
                  }
                  return NO;
              }
              
              @end
              

              【讨论】:

              • 很棒的解决方案!
              【解决方案10】:

              只是重复接受的答案并将其放在这里供我参考。接受的答案完美无缺。您可以像这样扩展它以允许您的视图的子视图接收触摸,或者将其传递给我们后面的任何视图:

              - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
                  // If one of our subviews wants it, return YES
                  for (UIView *subview in self.subviews) {
                      CGPoint pointInSubview = [subview convertPoint:point fromView:self];
                      if ([subview pointInside:pointInSubview withEvent:event]) {
                          return YES;
                      }
                  }
                  // otherwise return NO, as if userInteractionEnabled were NO
                  return NO;
              }
              

              注意:您甚至不必对子视图树进行递归,因为每个pointInside:withEvent: 方法都会为您处理。

              【讨论】:

                【解决方案11】:

                有几种方法可以处理这个问题。我最喜欢的是覆盖 hitTest:withEvent: 在一个常见的超级视图(可能是间接的)冲突视图(听起来像你称之为 A 和 B)。例如,像这样(这里 A 和 B 是 UIView 指针,其中 B 是“隐藏”的指针,通常被忽略):

                - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
                    CGPoint pointInB = [B convertPoint:point fromView:self];
                
                    if ([B pointInside:pointInB withEvent:event])
                        return B;
                
                    return [super hitTest:point withEvent:event];
                }
                

                您也可以按照 gyim 的建议修改 pointInside:withEvent: 方法。这可以让您通过有效地在 A 中“戳一个洞”来获得基本相同的结果,至少对于触摸来说是这样。

                另一种方法是事件转发,这意味着覆盖touchesBegan:withEvent: 和类似的方法(如touchesMoved:withEvent: 等)以将一些触摸发送到与它们最初到达的位置不同的对象。例如,在 A 中,你可以这样写:

                - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
                    if ([self shouldForwardTouches:touches]) {
                        [B touchesBegan:touches withEvent:event];
                    }
                    else {
                        // Do whatever A does with touches.
                    }
                }
                

                但是,这不会总是按您期望的方式工作!主要的是,像 UIButton 这样的内置控件将始终忽略转发的触摸。因此,第一种方法更可靠。

                有一篇很好的博客文章更详细地解释了所有这些,以及一个用于演示这些想法的小型工作 xcode 项目,可在此处获得:

                http://bynomial.com/blog/?p=74

                【讨论】:

                • 这个解决方案的问题 - 覆盖公共超级视图上的 hitTest - 是当底层视图是一组可滚动视图之一时,它不允许底层视图完全正常工作(MapView等等)。据我所知,下面的 gyim 建议在顶视图上覆盖 pointInside 在所有情况下都有效。
                • @delany,这不是真的;您可以在其他视图下方放置可滚动视图,并通过覆盖 hitTest 让它们都工作。这是一些示例代码:bynomial.com/blogfiles/Temp32.zip
                • 嘿。并非所有可滚动视图 - 只是一些......我从上面尝试了您的邮政编码,将 UIScrollView 更改为(例如)MKMapView 并且它不起作用。水龙头工作 - 似乎是滚动问题。
                • 好的,我检查了它并确认 hitTest 不能按照您对 MKMapView 的要求工作。你是对的,@delany;尽管它确实可以与 UIScrollView 一起正常工作。我想知道为什么 MKMapView 失败了?
                • @Tyler 正如您所提到的,“主要是像 UIButton 这样的内置控件将始终忽略转发的触摸”,我想知道您是如何知道的以及它是否是解释的官方文件对这种行为。我遇到了一个问题,当 UIButton 有一个普通的 UIView 子视图时,它不会响应子视图边界中的触摸事件。而且我发现该事件作为 UIView 的默认行为正确转发到 UIButton,但我不确定它是指定功能还是只是一个错误。请给我看一下这方面的文档好吗?非常感谢你。
                【解决方案12】:

                你必须设置upperView.userInteractionEnabled = NO;,否则上层视图会拦截触摸。

                Interface Builder 版本是 View Attributes 面板底部的一个复选框,名为“User Interaction Enabled”。取消选中它,你应该很高兴。

                【讨论】:

                • 对不起 - 应该说。我仍然希望能够与顶部 UIView 中的对象进行交互。
                • 但是upperView无法接收到任何触摸,将按钮包含在upperView中。
                • 这个解决方案对我有用。我根本不需要上视图来对触摸做出反应。
                【解决方案13】:

                禁用 userInteraction 属性可能会有所帮助。例如:

                UIView * topView = [[TOPView alloc] initWithFrame:[self bounds]];
                [self addSubview:topView];
                [topView setUserInteractionEnabled:NO];
                

                (注意:在上面的代码中,'self'指的是一个视图)

                这样,您只能在 topView 上显示,但不会获得用户输入。所有这些用户触摸都将通过此视图,底部视图将响应它们。我会使用这个 topView 来显示透明图像,或者为它们设置动画。

                【讨论】:

                  【解决方案14】:

                  我认为正确的方法是使用视图层次结构中内置的视图链。 对于推送到主视图的子视图,不要使用通用 UIView,而是使用 UIView 的子类(或其变体之一,如 UIImageView)来制作 MYView : UIView(或任何你想要的超类型,如 UIImageView)。在 YourView 的实现中,实现 touchesBegan 方法。然后在触摸该视图时调用此方法。在该实现中,您只需要一个实例方法:

                  - (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event ;
                  {   // cannot handle this event. pass off to super
                      [self.superview touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event]; }
                  

                  this touchesBegan 是一个响应者 api,所以你不需要在你的公共或私有接口中声明它;它是您必须了解的那些神奇 api 之一。这个 self.superview 最终会将请求冒泡到 viewController。然后,在 viewController 中实现这个 touchesBegan 来处理触摸。

                  请注意,触摸位置 (CGPoint) 会相对于包含视图自动调整,因为它会在视图层次链中向上反弹。

                  【讨论】:

                    【解决方案15】:

                    我想我参加这个聚会有点晚了,但我会添加这个可能的解决方案:

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

                    如果您使用此代码覆盖自定义 UIView 的标准 hitTest 函数,它将仅忽略视图本身。该视图的任何子视图都将正常返回其命中,而任何本应到达视图本身的命中都将传递给其父视图。

                    -灰

                    【讨论】:

                    • 这是我的首选方法,但我认为您不应该返回 [self superview]。有关此方法的文档状态“返回包含指定点的视图层次结构(包括其自身)中接收器的最远后代”和“如果该点完全位于接收器的视图层次结构之外,则返回 nil”。我认为你应该返回nil。当您返回 nil 时,控制权将传递给 superview 以检查它是否有任何命中。所以基本上它会做同样的事情,除了返回 superview 可能会在未来破坏某些东西。
                    • 当然,这可能是明智之举(请注意我原始答案中的日期 - 从那时起进行了更多编码)
                    【解决方案16】:

                    您可以采取一些措施来拦截两个视图中的触摸。

                    顶视图:

                    -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
                    {
                       // Do code in the top view
                       [bottomView touchesBegan:touches withEvent:event]; // And pass them on to bottomView
                       // You have to implement the code for touchesBegan, touchesEnded, touchesCancelled in top/bottom view.
                    }
                    

                    但就是这样。

                    【讨论】:

                    • 这当然是可能的——但工作量很大(我认为你必须在底层滚动自己的触敏对象(例如按钮)?)而且看起来很奇怪必须以这种方式滚动您自己的行为才能获得看似直观的行为。
                    • 我不确定我们是否应该试一试。
                    【解决方案17】:

                    我的解决方案:

                    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
                        CGPoint pointInView = [self.toolkitController.toolbar convertPoint:point fromView:self];
                    
                        if ([self.toolkitController.toolbar pointInside:pointInView withEvent:event]) {
                           self.userInteractionEnabled = YES;
                        } else {
                           self.userInteractionEnabled = NO;
                        }
                    
                        return [super hitTest:point withEvent:event];
                    }
                    

                    希望对你有帮助

                    【讨论】:

                      【解决方案18】:

                      您应该为您的顶视图创建一个 UIView 子类并覆盖以下方法:

                      - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
                          // UIView will be "transparent" for touch events if we return NO
                          return (point.y < MIDDLE_Y1 || point.y > MIDDLE_Y2);
                      }
                      

                      你也可以看看 hitTest:event: 方法。

                      【讨论】:

                      • 这里的middle_y1/y2代表什么?
                      • 不确定return 语句在做什么,但return CGRectContainsPoint(eachSubview.frame, point) 对我有用。否则非常有帮助的答案
                      • MIDDLE_Y1/Y2 业务只是一个例子。对于MIDDLE_Y1&lt;=y&lt;=MIDDLE_Y2 区域中的触摸事件,此函数将是“透明的”。
                      【解决方案19】:

                      我从未使用 UI 工具包构建过完整的用户界面,因此我对它没有太多经验。这是我认为应该起作用的方法。

                      每个 UIView,以及这个 UIWindow,都有一个属性subviews,它是一个包含所有子视图的 NSArray。

                      您添加到视图的第一个子视图将接收索引 0,下一个索引为 1,依此类推。您还可以将addSubview: 替换为insertSubview: atIndex:insertSubview:aboveSubview: 以及可以确定子视图在层次结构中的位置的此类方法。

                      所以请检查您的代码,看看您首先将哪个视图添加到您的 UIWindow。那将是 0,另一个将是 1。
                      现在,从您的一个子视图中,要到达另一个子视图,您将执行以下操作:

                      UIView * theOtherView = [[[self superview] subviews] objectAtIndex: 0];
                      // or using the properties syntax
                      UIView * theOtherView = [self.superview.subviews objectAtIndex:0];
                      

                      让我知道这是否适用于您的情况!


                      (这个标记下面是我之前的回答):

                      如果视图需要相互通信,它们应该通过控制器来实现(即使用流行的MVC 模型)。

                      当您创建一个新视图时,您可以确保它向控制器注册自己。

                      因此,技术是确保您的视图注册到控制器(可以按名称或您喜欢的任何内容存储在字典或数组中)。您可以让控制器为您发送消息,或者您可以获取对视图的引用并直接与其通信。

                      如果您的视图没有返回控制器的链接(可能是这种情况),那么您可以使用 singletons 和/或类方法来获取对控制器的引用。

                      【讨论】:

                      • 感谢您的回复 - 但我不确定我是否理解。两个视图都有一个控制器 - 问题是,一个视图在另一个视图之上,底部视图没有拾取事件(并将它们转发到它的控制器),即使没有对象实际上“阻止”这些事件顶视图。
                      • 在我们的 UIViews 顶部有一个 UIWindow 吗?如果这样做,则应该传播事件,并且您不需要做任何“魔术”。在 Apple 开发中心阅读 [Window and Views][1] (如果这对您没有帮助,请务必添加另一条评论!)[1]:developer.apple.com/iphone/library/documentation/iPhone/…
                      • 是的,绝对 - 层次结构顶部的 UIWindow。
                      • 我已经更新了我的答案以包含用于遍历视图层次结构的代码。如果您需要任何其他帮助,请告诉我!
                      • 感谢您的帮助 nash - 但我在构建这些接口方面经验丰富,据我所知,我当前的接口设置正确。问题似乎是完整视图置于其他视图之上时的默认行为。
                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2017-10-06
                      • 2012-11-04
                      • 1970-01-01
                      相关资源
                      最近更新 更多