允许在一个UIView下方与另一个UIView进行交互

116

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

例如,目前我有一个UIView(A),其屏幕顶部和底部都有一个对象,中间没有任何对象。这个UIView位于另一个具有中间按钮(B)的UIView上面。然而,我似乎无法与B中间的按钮进行交互。

我可以看到B中的按钮——我将A的背景设置为ClearColor——但是B中的按钮似乎无法接收触摸,尽管A中没有任何对象实际上在这些按钮的上面。

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

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


2
这里已经解释得很清楚了:http://developer.apple.com/iphone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/EventHandling/EventHandling.html 但基本上,重写hitTest:withEvent:方法即可,他们甚至提供了代码示例。 - nash
我写了一个小类专门处理这个问题。(在答案中添加了一个示例)。那里的解决方案比被接受的答案要好一些,因为你仍然可以点击半透明的UIView下面的UIButton,而UIView不透明的部分仍然会响应触摸事件。 - Segev
19个回答

96

你应该为顶部视图创建一个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: 方法。


19
这里的middle_y1/y2代表什么? - Jason Renaldo
不确定那个 return 语句在干什么,但 return CGRectContainsPoint(eachSubview.frame, point) 对我有用。除此之外,这是一个非常有帮助的答案。 - n00neimp0rtant
MIDDLE_Y1/Y2业务只是一个例子。此函数将在MIDDLE_Y1<=y<=MIDDLE_Y2区域的触摸事件中“透明”处理。 - gyim

44
虽然这里的许多答案都能起作用,但我有点惊讶地发现最方便、通用和万无一失的答案还没有被提出来。 @Ash 最接近了,除了返回 superview 时有些奇怪...不要那样做。
这个答案摘自我对类似问题的回答,链接在这里
- (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一样简单。


2
唯一正确的答案。明确地说,如果您想忽略对视图的触摸,但不是对包含在视图中的任何按钮(例如),请按照Stuart的说明操作。(我通常称其为“持有者视图”,因为它可以无害地“持有”一些按钮,但它不会影响“持有者”下面的任何内容。) - Fattie
其他使用点的解决方案存在问题,如果您的视图是可滚动的,例如UITableView或UICollectionView,并且您已向上或向下滚动。然而,这个解决方案可以在滚动时正常工作。 - pajevic

31

你可以采用多种方式来解决这个问题。我最喜欢的方法是在一个视图中重写hitTest:withEvent:,这个视图是冲突视图(也许是间接的)的共同父视图。例如,类似于以下代码(其中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
@delany,那不是真的;你可以让可滚动视图位于其他视图下面,并通过覆盖hitTest让它们同时工作。这是一些示例代码:http://bynomial.com/blogfiles/Temp32.zip - Tyler
嘿。并不是所有可滚动的视图 - 只有一些...我尝试了你上面提供的代码,将UIScrollView更改为(例如)MKMapView,但它不起作用。点击可以正常工作 - 滚动似乎是问题所在。 - delany
好的,我已经检查过了,并确认hitTest在MKMapView中不像你想的那样工作。你是对的,@delany;虽然它在UIScrollView中可以正常工作。我想知道为什么MKMapView会失败? - Tyler
@Tyler,正如你所提到的,“内置控件如UIButton将始终忽略转发的触摸”,我想知道你是如何知道这一点的,是否有官方文件来解释这种行为。我遇到了一个问题,当UIButton有一个普通的UIView子视图时,它将不会响应子视图边界内的触摸事件。我发现事件被正确地转发到UIButton,作为UIView的默认行为,但我不确定它是指定的特性还是一个错误。请问你能否向我展示相关文档?非常感谢。 - Neal.Marlin

29

要阻止上层视图拦截触摸事件,您需要设置upperView.userInteractionEnabled = NO;

在Interface Builder中,这个操作可以通过在“视图属性”面板底部找到名为“启用用户交互”的复选框并取消勾选来完成。


抱歉 - 我应该说一下。我仍然希望能够与顶部UIView中的对象进行交互。 - delany
但是upperView无法接收任何触摸,包括upperView中的按钮。 - imcaptor
2
这个解决方案对我很有效。我根本不需要上层视图对触摸做出反应。 - T.J.

11

对于pointInside:withEvent:的自定义实现似乎是正确的选择,但使用硬编码的坐标值让我感到奇怪。因此,我最终使用CGRectContainsPoint()函数来检查CGPoint是否在按钮CGRect内部:

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

10
最近,我编写了一个类来帮助我做到这一点。将其用作UIButtonUIView的自定义类将传递在透明像素上执行的触摸事件。
与接受的答案相比,此解决方案要好一些,因为您仍然可以单击半透明UIView下面的UIButton,而非透明部分的UIView仍将响应触摸事件。
如GIF所示,长颈鹿按钮是一个简单的矩形,但在透明区域上执行的触摸事件会传递给下面的黄色UIButton类链接

2
由于您的代码不是很长,建议您在回答中包含相关部分,以防将来项目被移动或删除。 - Gavin
谢谢,您的解决方案适用于我的情况,我需要非透明部分的UIView响应触摸事件,而透明部分则不需要。太棒了! - Bruce
@Bruce,很高兴能帮到你! - Segev

4

我可能有点晚了,但是我会提供一个可能的解决方案:

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

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

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

4

以下是我对“接受的答案”进行的拓展,供参考。这个答案已经完美解决了问题。您可以按照以下方式扩展它,以使您的视图子视图能够接收触摸事件或将其传递给我们后面的任何视图:

- (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:方法将为您处理。


4
这种方法非常简洁,可以让透明的子视图不受触摸的影响。只需对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

太棒了的解决方案! - Genevios

3

设置 userInteraction 属性为 disabled 可能会有帮助。例如:

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

(注意:上面的代码中,“self”指的是视图)
通过这种方式,您只能在topView上显示内容,但无法接收用户输入。所有这些用户触摸事件将穿过此视图并由底部视图对其进行响应。我会使用此topView来显示透明图像或对它们进行动画处理。

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