【问题标题】:How can I click a button behind a transparent UIView?如何单击透明 UIView 后面的按钮?
【发布时间】:2011-03-04 01:45:54
【问题描述】:

假设我们有一个带有一个子视图的视图控制器。子视图占据屏幕的中心,四周有 100 px 的边距。然后,我们在该子视图中添加了一些小东西来点击。我们只是使用子视图来利用新框架(子视图中的 x=0, y=0 实际上是父视图中的 100,100)。

然后,想象我们在子视图后面有一些东西,比如菜单。我希望用户能够在子视图中选择任何“小东西”,但如果那里什么都没有,我希望触摸通过它(因为无论如何背景都是清晰的)到它后面的按钮。

我该怎么做?看起来 touchesBegan 通过了,但按钮不起作用。

【问题讨论】:

  • 我认为透明(alpha 0)的 UIView 不应该响应触摸事件?
  • 我为此编写了一个小班。 (在答案中添加了一个示例)。那里的解决方案比接受的答案要好一些,因为您仍然可以单击半透明UIView 下的UIButton,而UIView 的非透明部分仍将响应触摸事件。

标签: ios objective-c iphone cocoa-touch ipad


【解决方案1】:

我使用它而不是覆盖方法点(内部:CGPoint,带有:UIEvent)

  override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard self.point(inside: point, with: event) else { return nil }
        return self
    }

【讨论】:

    【解决方案2】:

    更简单的方法是在界面构建器中“取消选中”用户交互启用。 “如果您使用的是故事板”

    【讨论】:

    • 正如其他人在类似答案中指出的那样,这在这种情况下无济于事,因为它还会在底层视图中禁用触摸事件。换句话说:无论您启用还是禁用此设置,都无法点击该视图下方的按钮。
    【解决方案3】:

    尝试将transparentView 中的backgroundColor 设置为UIColor(white:0.000, alpha:0.020)。然后您可以在touchesBegan/touchesMoved 方法中获取触摸事件。将下面的代码放在你的视图被初始化的地方:

    self.alpha = 1
    self.backgroundColor = UIColor(white: 0.0, alpha: 0.02)
    self.isMultipleTouchEnabled = true
    self.isUserInteractionEnabled = true
    

    【讨论】:

      【解决方案4】:

      试试这个

      class PassthroughToWindowView: UIView {
              override func test(_ point: CGPoint, with event: UIEvent?) -> UIView? {
                  var view = super.hitTest(point, with: event)
                  if view != self {
                      return view
                  }
      
                  while !(view is PassthroughWindow) {
                      view = view?.superview
                  }
                  return view
              }
          } 
      

      【讨论】:

        【解决方案5】:

        为您的容器创建一个自定义视图并覆盖 pointInside: 消息以在该点不在符合条件的子视图中时返回 false,如下所示:

        斯威夫特:

        class PassThroughView: UIView {
            override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
                for subview in subviews {
                    if !subview.isHidden && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
                        return true
                    }
                }
                return false
            }
        }
        

        目标 C:

        @interface PassthroughView : UIView
        @end
        
        @implementation PassthroughView
        -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
            for (UIView *view in self.subviews) {
                if (!view.hidden && view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event])
                    return YES;
            }
            return NO;
        }
        @end
        

        将此视图用作容器将允许其任何子级接收触摸,但视图本身将对事件透明。

        【讨论】:

        • 有趣。我需要深入了解响应者链。
        • 您需要检查视图是否也可见,然后您还需要在测试之前检查子视图是否可见。
        • 不幸的是,这个技巧不适用于 tabBarController,因为它的视图不能改变。任何人都想让这个视图对事件透明吗?
        • 您还应该检查视图的 alpha。我经常通过将 alpha 设置为零来隐藏视图。零 alpha 的视图应该像隐藏视图一样。
        • 我做了一个快速版本:也许你可以将它包含在可见性的答案中gist.github.com/eyeballz/17945454447c7ae766cb
        【解决方案6】:

        票数最高的解决方案对我来说并不完全有效,我想这是因为我在层次结构中有一个 TabBarController(正如其中一位 cmets 指出的那样),它实际上是将触摸传递到 UI 的某些部分,但它是弄乱了我的 tableView 拦截触摸事件的能力,它最终是在视图中覆盖 hitTest 我想忽略触摸并让该视图的子视图处理它们

        - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
            UIView *view = [super hitTest:point withEvent:event];
            if (view == self) {
                return nil; //avoid delivering touch events to the container view (self)
            }
            else{
                return view; //the subviews will still receive touch events
            }
        }
        

        【讨论】:

        • 这应该是答案。超级干净。
        【解决方案7】:

        斯威夫特 3

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

        【讨论】:

          【解决方案8】:

          如果您懒得使用类别或子类 UIView,您也可以将按钮向前拉,使其位于透明视图的前面。根据您的应用程序,这并不总是可能的,但它对我有用。您可以随时将按钮重新拉回或将其隐藏。

          【讨论】:

          • 嗨,欢迎来到堆栈溢出。对您作为新用户的提示:您可能需要注意自己的语气。我会避免使用诸如“如果您不能打扰”之类的短语;它可能显得居高临下
          • 感谢您的提醒。从懒惰的角度来看,这比居高临下,但重点是。
          【解决方案9】:

          From Apple:

          事件转发是一些应用程序使用的一种技术。您通过调用另一个响应者对象的事件处理方法来转发触摸事件。尽管这可能是一种有效的技术,但您应该谨慎使用它。 UIKit 框架的类并非旨在接收未绑定到它们的触摸......如果您想有条件地将触摸转发给应用程序中的其他响应者,所有这些响应者都应该是您自己的 UIView 子类的实例。

          Apples Best Practise:

          不要显式地将事件发送到响应者链(通过 nextResponder);相反,调用超类实现并让 UIKit 处理响应者链遍历。

          相反,您可以覆盖:

          -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
          

          在您的 UIView 子类中,如果您希望将触摸发送到响应者链(即到您的视图后面的视图,其中没有任何内容),则返回 NO。

          【讨论】:

          • 如果有一个指向您引用的文档的链接以及引用本身,那就太棒了。
          • 我为您添加了文档中相关位置的链接
          • ~~你能展示一下覆盖应该是什么样子吗?~~找到一个answer in another question,它会是什么样子;它实际上只是返回NO;
          • 这应该是公认的答案。这是最简单的方法,也是推荐的方法。
          【解决方案10】:

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

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

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

          Link to class

          【讨论】:

          • 虽然此解决方案可能有效,但最好将解决方案的相关部分放在答案中。
          【解决方案11】:

          我为此创建了一个类别。

          一个小方法混搭,视图是金色的。

          标题

          //UIView+PassthroughParent.h
          @interface UIView (PassthroughParent)
          
          - (BOOL) passthroughParent;
          - (void) setPassthroughParent:(BOOL) passthroughParent;
          
          @end
          

          实现文件

          #import "UIView+PassthroughParent.h"
          
          @implementation UIView (PassthroughParent)
          
          + (void)load{
              Swizz([UIView class], @selector(pointInside:withEvent:), @selector(passthroughPointInside:withEvent:));
          }
          
          - (BOOL)passthroughParent{
              NSNumber *passthrough = [self propertyValueForKey:@"passthroughParent"];
              if (passthrough) return passthrough.boolValue;
              return NO;
          }
          - (void)setPassthroughParent:(BOOL)passthroughParent{
              [self setPropertyValue:[NSNumber numberWithBool:passthroughParent] forKey:@"passthroughParent"];
          }
          
          - (BOOL)passthroughPointInside:(CGPoint)point withEvent:(UIEvent *)event{
              // Allow buttons to receive press events.  All other views will get ignored
              if (self.passthroughParent){
                  if (self.alpha != 0 && !self.isHidden){
                      for( id foundView in self.subviews )
                      {
                          if ([foundView alpha] != 0 && ![foundView isHidden] && [foundView pointInside:[self convertPoint:point toView:foundView] withEvent:event])
                              return YES;
                      }
                  }
                  return NO;
              }
              else {
                  return [self passthroughPointInside:point withEvent:event];// Swizzled
              }
          }
          
          @end
          

          您需要添加我的 Swizz.h 和 Swizz.m

          位于Here

          之后,你只需在你的 {Project}-Prefix.pch 文件中导入 UIView+PassthroughParent.h,每个视图都会有这个能力。

          每个视图都会拿分,但没有一个空格会。

          我还建议使用清晰的背景。

          myView.passthroughParent = YES;
          myView.backgroundColor = [UIColor clearColor];
          

          编辑

          我创建了自己的财产包,以前没有。

          头文件

          // NSObject+PropertyBag.h
          
          #import <Foundation/Foundation.h>
          
          @interface NSObject (PropertyBag)
          
          - (id) propertyValueForKey:(NSString*) key;
          - (void) setPropertyValue:(id) value forKey:(NSString*) key;
          
          @end
          

          实施文件

          // NSObject+PropertyBag.m
          
          #import "NSObject+PropertyBag.h"
          
          
          
          @implementation NSObject (PropertyBag)
          
          + (void) load{
              [self loadPropertyBag];
          }
          
          + (void) loadPropertyBag{
              @autoreleasepool {
                  static dispatch_once_t onceToken;
                  dispatch_once(&onceToken, ^{
                      Swizz([NSObject class], NSSelectorFromString(@"dealloc"), @selector(propertyBagDealloc));
                  });
              }
          }
          
          __strong NSMutableDictionary *_propertyBagHolder; // Properties for every class will go in this property bag
          - (id) propertyValueForKey:(NSString*) key{
              return [[self propertyBag] valueForKey:key];
          }
          - (void) setPropertyValue:(id) value forKey:(NSString*) key{
              [[self propertyBag] setValue:value forKey:key];
          }
          - (NSMutableDictionary*) propertyBag{
              if (_propertyBagHolder == nil) _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100];
              NSMutableDictionary *propBag = [_propertyBagHolder valueForKey:[[NSString alloc] initWithFormat:@"%p",self]];
              if (propBag == nil){
                  propBag = [NSMutableDictionary dictionary];
                  [self setPropertyBag:propBag];
              }
              return propBag;
          }
          - (void) setPropertyBag:(NSDictionary*) propertyBag{
              if (_propertyBagHolder == nil) _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100];
              [_propertyBagHolder setValue:propertyBag forKey:[[NSString alloc] initWithFormat:@"%p",self]];
          }
          
          - (void)propertyBagDealloc{
              [self setPropertyBag:nil];
              [self propertyBagDealloc];//Swizzled
          }
          
          @end
          

          【讨论】:

          • passthroughPointInside 方法对我来说效果很好(即使不使用任何 passthroughparent 或 swizz 的东西 - 只需将 passthroughPointInside 重命名为 pointInside),非常感谢。
          • 什么是propertyValueForKey?
          • 嗯。之前没有人指出这一点。我构建了一个自定义指针字典,其中包含类扩展中的属性。我会看看我能不能找到它把它包括在这里。
          • 众所周知,Swizzling 方法是一种微妙的黑客解决方案,可用于罕见情况和开发人员的乐趣。这绝对不是 App Store 安全的,因为它很可能会在下一次操作系统更新时破坏和崩溃您的应用程序。为什么不直接覆盖pointInside: withEvent:
          【解决方案12】:

          从其他答案中获取提示并阅读 Apple 的文档,我创建了这个简单的库来解决您的问题:
          https://github.com/natrosoft/NATouchThroughView
          它使在 Interface Builder 中绘制视图变得容易,这些视图应该将触摸传递到底层视图。

          我认为在生产代码中进行方法调配是多余的,而且非常危险,因为您直接扰乱了 Apple 的基本实现并进行了可能导致意外后果的应用程序范围的更改。

          有一个演示项目,希望自述文件能很好地解释要做什么。为了解决 OP,您可以在 Interface Builder 中将包含按钮的清晰 UIView 更改为类 NATouchThroughView。然后找到覆盖您想要点击的菜单的清晰 UIView。在 Interface Builder 中将该 UIView 更改为 NARootTouchThroughView 类。如果您打算将这些触摸传递到底层视图控制器,它甚至可以是您的视图控制器的根 UIView。查看演示项目以了解它是如何工作的。真的很简单,安全,无创

          【讨论】:

            【解决方案13】:

            基于 John 发布的内容,这里有一个示例,它允许触摸事件通过视图的所有子视图(按钮除外):

            -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
            {
                // Allow buttons to receive press events.  All other views will get ignored
                for( id foundView in self.subviews )
                {
                    if( [foundView isKindOfClass:[UIButton class]] )
                    {
                        UIButton *foundButton = foundView;
            
                        if( foundButton.isEnabled  &&  !foundButton.hidden &&  [foundButton pointInside:[self convertPoint:point toView:foundButton] withEvent:event] )
                            return YES;
                    }        
                }
                return NO;
            }
            

            【讨论】:

            • 需要检查视图是否可见,子视图是否可见。
            • 完美解决方案!一个更简单的方法是创建一个UIView 子类PassThroughView,如果你想将任何触摸事件转发到下面的视图,它只会覆盖-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }
            【解决方案14】:

            我也用过

            myView.userInteractionEnabled = NO;
            

            不需要子类。工作正常。

            【讨论】:

            • 这也会禁用任何子视图的用户交互
            • 正如@pixelfreak 提到的,这不是所有情况的理想解决方案。如果仍需要该视图的子视图上的交互,则需要该标志保持启用状态。
            【解决方案15】:

            根据“iPhone 应用程序编程指南”:

            关闭触摸事件的传递。 默认情况下,视图接收触摸 事件,但您可以将其 userInteractionEnabled 属性设置为 NO 关闭事件的传递。 如果视图被隐藏,它也不会接收事件 或者如果它是透明的

            http://developer.apple.com/iphone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/EventHandling/EventHandling.html

            更新:删除示例 - 重新阅读问题...

            在按钮获取之前,您是否对可能正在处理点击的视图进行了任何手势处理?当您没有透明视图时,该按钮是否有效?

            任何代码示例的非工作代码?

            【讨论】:

            • 是的,我在我的问题中写道,正常的触摸在视图下方起作用,但 UIButtons 和其他元素不起作用。
            【解决方案16】:

            据我所知,您应该能够通过覆盖hitTest: 方法来做到这一点。我确实尝试过,但无法使其正常工作。

            最后,我在可触摸对象周围创建了一系列透明视图,这样它们就不会覆盖它。我的问题有点破解,这很好用。

            【讨论】:

              猜你喜欢
              • 2013-03-30
              • 2020-05-02
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2016-01-30
              相关资源
              最近更新 更多