超出UIView范围的交互

102

一个UIButton(或其他任何控件)是否可能在它的frame超出其父视图的范围时接收触摸事件?因为当我尝试这样做时,我的UIButton似乎无法接收任何事件。如何解决这个问题?


1
这对我来说非常完美。https://developer.apple.com/library/ios/qa/qa2013/qa1812.html 最好的。 - Frank Li
2
这也很好而且相关(或者可能是重复的):https://dev59.com/e2445IYBdhLWcg3wQX-G#31420820 - Dan Rosenstark
10个回答

97

是的。您可以重写hitTest:withEvent:方法,以返回一个包含更多点的视图。请参见UIView Class Reference

编辑:示例:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat radius = 100.0;
    CGRect frame = CGRectMake(-radius, -radius,
                              self.frame.size.width + radius,
                              self.frame.size.height + radius);

    if (CGRectContainsPoint(frame, point)) {
        return self;
    }
    return nil;
}

编辑2:(澄清后:)为了确保按钮被视为在父视图范围内,您需要在父视图中覆盖pointInside:withEvent:方法,并包括按钮的框架。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    if (CGRectContainsPoint(self.view.bounds, point) ||
        CGRectContainsPoint(button.view.frame, point))
    {
        return YES;
    }
    return NO;
}

注意:仅仅用于覆盖pointInside的代码并不完全正确。如下所述,按照Summon的解释,应该这样做:

Note the code just there for overriding pointInside is not quite correct. As Summon explains below, do this:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    {
    if ( CGRectContainsPoint(self.oversizeButton.frame, point) )
        return YES;

    return [super pointInside:point withEvent:event];
    }

注意,在这个UIView子类中,你很可能会用self.oversizeButton作为IBOutlet来完成它;然后你只需要将所涉及的“超大按钮”拖到特定的视图中。 (或者,如果出于某种原因您在项目中经常这样做,那么您可以有一个特殊的UIButton子类,并且您可以查看子视图列表以获得这些类。)希望能帮到你。


你能否帮我进一步实现这个功能并提供一些示例代码?此外,我读了这个引用,因为以下语句,我感到困惑:“超出接收器范围的点永远不会被报告为命中,即使它们实际上位于接收器的某个子视图中。如果父视图的 clipsToBounds 属性设置为 NO,则子视图可以在视觉上超出其父视图的边界。然而,命中测试始终忽略父视图边界之外的点。” - ThomasM
啊,我明白了。你需要重写父类的 pointInside:withEvent: 方法来包含按钮的框架。我会在上面的答案中添加一些示例代码。 - jnic
关于这个解决方案还有一个需要注意的地方。在 hitTest:withEvent: 方法中,你应该返回与点击测试匹配的最低子视图。这意味着,如果你有一个超出父视图边界的按钮,你会首先命中测试该按钮并返回它,然后只需调用 'return [super hitTest:point withEvent:event];' 即可。 - Jeremy Wiebe
方法没有被调用。 - Iulian Onofrei
1
当我按下一个位于父视图边界之外的按钮时,hitTest:withEvent:pointInside:withEvent:方法没有被调用。可能出了什么问题? - iosdude
显示剩余5条评论

24

@jnic,我正在使用iOS SDK 5.0工作,为了让你的代码正常工作,我不得不这样做:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint(button.frame, point)) {
    return YES;
}
return [super pointInside:point withEvent:event]; }
在我的情况下,容器视图是一个UIButton,所有子元素也是UIButtons,它们可以移动到超出父UIButton边界的位置。
最好的。

23

在我的情况下,我有一个包含UIButtonUICollectionViewCell子类。 我在单元格上禁用了clipsToBounds,并且按钮可见超出了单元格的边界。 但是,该按钮没有接收到触摸事件。 我能够使用Jack的答案检测到按钮上的触摸事件:https://dev59.com/L2035IYBdhLWcg3wbPU5#30431157

这里是Swift版本:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {

    let translatedPoint = button.convertPoint(point, fromView: self)

    if (CGRectContainsPoint(button.bounds, translatedPoint)) {
        print("Your button was pressed")
        return button.hitTest(translatedPoint, withEvent: event)
    }
    return super.hitTest(point, withEvent: event)
}

Swift 4:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

    let translatedPoint = button.convert(point, from: self)

    if (button.bounds.contains(translatedPoint)) {
        print("Your button was pressed")
        return button.hitTest(translatedPoint, with: event)
    }
    return super.hitTest(point, with: event)
}

那正是我的情况。另一个要点是,你应该把这个方法放在CollectionViewCell类里面。 - Hamid Reza Ansari
但是为什么要将按钮注入到重写的方法中呢? - rommex

22
在父视图中,您可以重写命中测试方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGPoint translatedPoint = [_myButton convertPoint:point fromView:self];

    if (CGRectContainsPoint(_myButton.bounds, translatedPoint)) {
        return [_myButton hitTest:translatedPoint withEvent:event];
    }
    return [super hitTest:point withEvent:event];

}
在这种情况下,如果点落在按钮范围内,您会将调用转发到那里;否则,恢复到原始实现。

11

为什么会发生这种情况?

原因在于当你的子视图超出父视图的边界时,实际上发生在该子视图上的触摸事件将无法传递给该子视图。但是,它传递给它的父视图。

无论子视图是否在视觉上被剪裁,触摸事件始终尊重目标视图的父视图边界矩形。换句话说,在视图的一部分发生的触摸事件,如果该部分位于其父视图的边界矩形之外,则不会传递到该视图。 链接

你需要做什么?

当你的父视图接收到上述的触摸事件时,你需要显式地告诉UIKit,我的子视图应该是接收此触摸事件的那个视图。

代码怎么写?

在你的父视图中,实现func hitTest(_ point: CGPoint, with event: UIEvent?)方法。

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if isHidden || alpha == 0 || clipsToBounds { return super.hitTest(point, with: event) }
        // convert the point into subview's coordinate system
        let subviewPoint = self.convert(point, to: subview)
        // if the converted point lies in subview's bound, tell UIKit that subview should be the one that receives this event
        if !subview.isHidden && subview.bounds.contains(subviewPoint) { return subview }
        return super.hitTest(point, with: event)
    }

有趣的问题:你必须去到“最高的太小父视图”

你需要向上进入包含问题视图的最高层视图。

典型的例子:

假设你有一个名为S的屏幕,其中包含一个容器视图C。容器视图控制器的视图是V。(请注意V将位于C内并具有相同的大小。)V有一个子视图(可能是一个按钮)B。B是实际上在V之外的问题视图。

但请注意,B也在C之外。

在这个例子中,你需要将解决方案override hitTest应用到C中,而不是应用到V中。如果应用到V中,则无效。


4
谢谢你!这个问题已经让我花费了数小时,它也是从这个问题引发的。非常感谢你抽出时间来发布这个帖子。 :) - ClayJ
@ScottZhu: 我在你的答案中添加了一个重要的注意事项。当然,你可以随意删除或更改我的添加部分!再次感谢! - Fattie

9

Swift:

其中targetView是您希望接收事件的视图(可能完全或部分地位于其父视图的框架之外)。

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        let pointForTargetView: CGPoint = targetView.convertPoint(point, fromView: self)
        if CGRectContainsPoint(targetView.bounds, pointForTargetView) {
            return closeButton
        }
        return super.hitTest(point, withEvent: event)
}

7

对于我和其他人而言,答案不是覆盖hitTest方法,而是覆盖pointInside方法。我只需要在两个地方进行修改。

超级视图 如果将clipsToBounds设置为true,则此视图就可以解决整个问题。因此,以下是我使用Swift 3编写的简单UIView代码:

class PromiscuousView : UIView {
    override func point(inside point: CGPoint,
               with event: UIEvent?) -> Bool {
        return true
    }
} 

子视图 可能因人而异,但在我的情况下,我还必须覆盖子视图的pointInside方法,以判断触摸是否超出边界。我的是用Objective-C编写的,所以:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    WEPopoverController *controller = (id) self.delegate;
    if (self.takeHitsOutside) {
        return YES;
    } else {
        return [super pointInside:point withEvent:event];
    }
}

这个答案(以及同一问题的几个其他答案)可以帮助您澄清理解。


2

Swift 4.1已更新

要在同一UIView中获取触摸事件,您只需要添加以下重写方法,它将识别UIView中超出范围的特定视图被点击:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let pointforTargetview:CGPoint = self.convert(point, to: btnAngle)
    if  btnAngle.bounds.contains(pointforTargetview) {
        return  self
    }
    return super.hitTest(point, with: event)
}

2

我知道这是一个非常老的问题,但我们仍然需要这些解决方案作为更新版本,这就是为什么我会分享这个问题的新版本解决方案。

假设我的mainView框架是CGRect(0.0, 0.0, 250, 250.0),我想在其中添加一个大小为50的按钮 subView (位于左上角),它的框架看起来像这样CGRect(-50, -50, 50, 50) 在这种情况下,我们无法与此按钮进行交互,以下是如何解决此问题的示例

在您的MainView(按钮容器视图)中重写hitTest函数,如其他答案中所述,但在此处反转子视图顺序并将点击点转换为此子视图点,然后再次使用hitTest检查结果,否则检查您的视图是否包含该点返回您的mainView,否则返回nil以避免影响其他视图

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard !clipsToBounds && !isHidden && alpha > 0 else { return self.superview }
    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 self.bounds.contains(point) ? self : nil
}

我已经在自己的应用程序中测试和使用了这段代码,它运行良好。

0

以下是jnic's answer的Swift 5版本:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let radius: CGFloat = 100

    let frame = CGRect(
        x: -radius,
        y: -radius,
        width: frame.width + radius * 2,
        height: frame.height + radius * 2
    )

    if frame.contains(point) {
        return self
    }
    
    return nil
}

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