如何在透明的UIView后面点击按钮?

202
假设我们有一个视图控制器和一个子视图。子视图在屏幕中心占据位置,并在四周留出100像素的边距。然后我们添加了一堆小东西在这个子视图里面可以点击。我们只是利用子视图来利用新的框架(在子视图内部x=0,y=0实际上是在父视图中的100,100)。
然后,想象一下我们在子视图后面放置了某些东西,比如菜单。我希望用户能够选择子视图中的任何“小东西”,但如果没有,则希望触摸事件可以穿过子视图(因为背景本来就是透明的),传递到其后面的按钮。
我该怎么做呢?看起来touchesBegan可以通过,但按钮却不起作用。

1
我以为透明的(alpha 0)UIView不应该响应触摸事件? - Evadne Wu
1
我写了一个小类专门处理这个问题。(在答案中添加了一个示例)。那里的解决方案比被接受的答案要好一些,因为你仍然可以点击半透明的UIView下面的UIButton,而UIView不透明的部分仍然会响应触摸事件。 - Segev
注意:自iOS 14以来,UIStackView是一个渲染视图。这意味着它可以有一个背景。即使它是.clear颜色,它也不会将触摸事件传递给底层视图。 - heyfrank
16个回答

340

创建一个自定义视图用于容器,并覆盖pointInside:方法,当点不在符合条件的子视图内时返回false,像这样:

Swift:

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

Objective 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

将这个视图作为容器使用,可以使其任何子元素都能接收到触摸事件,但是视图本身对事件是透明的。


5
有趣。我需要更深入地了解响应者链。 - Sean Clark Hess
2
不幸的是,这个技巧对于tabBarController无效,因为视图无法更改。有人有什么想法可以使这个视图对事件透明吗? - cyrilchampier
1
你还应该检查视图的 alpha 值。很多时候,我会通过将 alpha 设置为零来隐藏一个视图。alpha 值为零的视图应该像隐藏视图一样运作。 - James Andrews
2
我写了一个Swift版本:也许你可以将它包含在答案中以增加可见度。 https://gist.github.com/eyeballz/17945454447c7ae766cb - eyeballz
1
这个答案真的帮了我,注意:你需要在容器和嵌入视图中都设置PassThroughView。 - Sruit A.Suk
显示剩余6条评论

128

我也使用

myView.userInteractionEnabled = NO;

不需要子类化。可以正常工作。


96
这也会禁用任何子视图的用户交互。 - pixelfreak
1
正如@pixelfreak所提到的,这并不是所有情况下的理想解决方案。对于仍需要与该视图的子视图进行交互的情况,需要保持该标志启用。 - Augusto Carmo

20

苹果官方文档

 

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

苹果最佳实践:

 

不要显式地通过 nextResponder 将事件发送给响应者链上的下一个响应者;相反,调用超类实现并让 UIKit 处理响应者链遍历。

如果需要,您可以覆盖:

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

在您的UIView子类中重写该方法,如果要将触摸事件发送到响应者链(即到其后面没有任何内容的视图),则返回NO。


1
带有引用的文档链接会非常棒。 - morgancodes
1
我已经为你添加了相关文档中的链接。 - jackslash
1
你能展示一下这个覆盖应该是什么样子的吗?在另一个问题的答案中找到了如何实现它的方法;它只需要简单地返回“NO”即可。 - kontur
这可能是被接受的答案。这是最简单和推荐的方法。 - Ecuador

9
一种更简单的方法是在界面构建器中“取消勾选”用户交互启用。如果您使用的是故事板,请按照以下步骤操作:enter image description here

13
正如其他人在类似的答案中指出的那样,这对此情况没有帮助,因为它还会禁用底层视图中的触摸事件。换句话说,无论您启用还是禁用此设置,下面那个视图下面的按钮都无法被点击。 - auco

7

对我而言,得票最高的解决方案不完全适用,我猜测这是因为我将一个TabBarController添加到了层次结构中(正如某些评论所指出的那样)。事实上,它确实将触摸事件传递给了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
    }
}

这应该是答案。非常干净。 - solderspot

6

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

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

6
最近我编写了一个类来帮助我完成这个任务。使用该类作为UIButtonUIView的自定义类将传递在透明像素上执行的触摸事件。
这个解决方案比被接受的答案更好,因为你仍然可以点击半透明UIView下面的UIButton,同时非透明部分的UIView仍然会响应触摸事件。
如GIF所示,长颈鹿按钮是一个简单的矩形,但透明区域上的触摸事件被传递到下面的黄色UIButton类链接

2
虽然这个解决方案可能有效,但最好将解决方案的相关部分放在答案中。 - Koen.

4

Swift 3

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

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

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

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

更新:删除示例 - 请重新阅读问题...
您的视图上是否有任何手势处理,可能会在按钮获取它之前处理轻拍?当您没有透明视图覆盖在按钮上时,按钮是否可用?
有任何不起作用的代码示例吗?

是的,我在我的问题中写道普通的触摸在底部视图中可以正常工作,但UIButtons和其他元素不起作用。 - Sean Clark Hess

1
我创建了一个类别来实现这个。
稍微进行一些方法交换(view swizzling),视图就会变得非常漂亮。
头文件。
//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
位于这里 之后,您只需在{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),非常感谢。 - Benjamin Piette
propertyValueForKey是什么? - Skotch
1
哦,之前还没有人指出这个问题。我构建了一个自定义指针字典,用于在类扩展中保存属性。我会看看能否找到它并将其包含在此处。 - The Lazy Coder
Swizzling方法被认为是一种对于罕见情况和开发者乐趣的微妙黑客解决方案。它绝对不是App Store安全的,因为很可能会在下一个操作系统更新中破坏和崩溃您的应用程序。为什么不只是覆盖pointInside: withEvent:呢? - auco

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