使用hitTest:withEvent捕获子视图在其父视图框架之外的触摸:


94

我的问题:我有一个超级视图EditView,它基本上占据了整个应用程序框架,一个子视图MenuView只占据了底部约20%的位置,然后MenuView包含了一个自己的子视图ButtonView,该子视图实际上位于MenuViews的范围之外(类似这样:)ButtonView.frame.origin.y = -100

(注意:EditView具有不属于MenuView视图层次结构的其他子视图,但可能会影响答案。)

您可能已经知道了这个问题:何时ButtonView在的范围内MenuView(或更具体地说,当我的触摸在MenuView的范围内时),ButtonView对触摸事件做出响应。当我的触摸不在MenuView的范围内(但仍在ButtonView的范围内)时,不会收到任何触摸事件ButtonView

例:

  • (E)是EditView,所有视图的父级
  • (M)是MenuViewEditView的子视图
  • (B)是ButtonViewMenuView的子视图

图解:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

因为(B)在(M)的框架之外,所以(B)区域中的轻击将永远不会发送到(M)-实际上,在这种情况下(M)绝不会分析触摸,并且将触摸发送到层次结构中的下一个对象。

目标:我认为重写hitTest:withEvent:可以解决此问题,但我不完全知道该怎么做。就我而言,应该hitTest:withEvent:EditView(我的“主”超级视图)中覆盖?还是应将其覆盖MenuView,不接受触摸的按钮的直接视图?还是我在想这个错误?

如果这需要冗长的解释,那么很好的在线资源将对您有所帮助-苹果公司的UIView文档除外,但我对此并不清楚。

谢谢!

Answers:


145

我将接受的答案的代码修改为更通用-它处理视图确实将子视图裁剪到其边界,可能被隐藏的情况,更重要的是:如果子视图是复杂的视图层次结构,则将返回正确的子视图。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if (self.clipsToBounds) {
        return nil;
    }

    if (self.hidden) {
        return nil;
    }

    if (self.alpha == 0) {
        return nil;
    }

    for (UIView *subview in self.subviews.reverseObjectEnumerator) {
        CGPoint subPoint = [subview convertPoint:point fromView:self];
        UIView *result = [subview hitTest:subPoint withEvent:event];

        if (result) {
            return result;
        }
    }

    return nil;
}

SWIFT 3

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

    if clipsToBounds || isHidden || alpha == 0 {
        return nil
    }

    for subview in subviews.reversed() {
        let subPoint = subview.convert(point, from: self)
        if let result = subview.hitTest(subPoint, with: event) {
            return result
        }
    }

    return nil
}

我希望这对尝试将这种解决方案用于更复杂的用例的人有所帮助。


这看起来不错,并感谢您这样做。但是,有一个问题:为什么要返回[super hitTest:point withEvent:event];?如果没有子视图触发触摸,您是否会只返回nil?苹果表示,如果没有子视图包含触摸,则hitTest返回nil。
Ser Pounce

嗯...是的,听起来差不多。同样,为了获得正确的行为,需要以相反的顺序迭代对象(因为最后一个在视觉上是最顶层的)。编辑的代码要匹配。
诺姆

3
我刚刚使用您的解决方案来使UIButton捕获触摸,并且它位于UICollectionViewCell内部(显然是UICollectionView)中的UIView内部。我必须将UICollectionView和UICollectionViewCell子类化,以覆盖这三个类上的hitTest:withEvent:。它的工作就像魅力!谢谢 !!
DanielGarcía'13

3
根据所需的用途,它应该返回[super hitTest:point withEvent:event]或为nil。回归自我会使它接受一切。
Noam 2014年

1
这是苹果公司关于同一技术的问答技术文档:developer.apple.com/library/ios/qa/qa2013/qa1812.html
James Kuang 2015年

33

好的,我做了一些挖掘和测试,下面是hitTest:withEvent工作原理-至少是在较高水平上。对此场景进行镜像:

  • (E)是EditView,所有视图的父级
  • (M)是MenuView,是EditView的子视图
  • (B)是ButtonView,是MenuView的子视图

图解:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

因为(B)在(M)的框架之外,所以(B)区域中的轻击将永远不会发送到(M)-实际上,在这种情况下(M)绝不会分析触摸,并且将触摸发送到层次结构中的下一个对象。

但是,如果您hitTest:withEvent:在(M)中实现,则应用程序中任意位置的点击都会发送到(M)(或者它至少不了解它们)。在这种情况下,您可以编写代码来处理触摸,然后返回应该接收触摸的对象。

更具体地说:的目标hitTest:withEvent:是返回应该受到点击的对象。因此,在(M)中,您可能会编写如下代码:

// need this to capture button taps since they are outside of self.frame
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{   
    for (UIView *subview in self.subviews) {
        if (CGRectContainsPoint(subview.frame, point)) {
            return subview;
        }
    }

    // use this to pass the 'touch' onward in case no subviews trigger the touch
    return [super hitTest:point withEvent:event];
}

对于这种方法和问题,我还是很陌生,因此,如果有更高效或更正确的代码编写方法,请发表评论。

希望对以后遇到此问题的其他人有所帮助。:)


谢谢,这对我有用,尽管我不得不做更多的逻辑以确定哪些子视图应该实际接收匹配。我从您的示例中删除了不必要的中断。
Daniel Saidi 2014年

当子视图框是父视图的边框时,单击事件没有响应!您的解决方案解决了这个问题。非常感谢:-)
Jeevan

@toblerpwn(有趣的昵称:)),您应该编辑这个更老的答案,以使其非常清楚(使用大胆的大写字母)您必须在哪个类中添加它。干杯!
Fattie

26

在Swift 5中

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

仅当您将替代应用于“行为异常”视图的直接
超级

2

我要做的是通过将ButtonView和MenuView都放置在框架完全适合它们的容器中,使它们都在视图层次结构中处于同一级别。这样,由于其是Superview的边界,因此剪切项的交互区域将不会被忽略。


我想过这个解决方法,以及-这意味着我将不得不重复一些布局的逻辑,但它可能确实是到底我最好的选择。(或重构一些严重的代码!)
toblerpwn

1

如果您的父视图中还有许多其他子视图,那么使用上述解决方案可能会导致大多数其他交互式视图不起作用,在这种情况下,您可以使用以下方法(在Swift 3.2中):

class BoundingSubviewsViewExtension: UIView {

    @IBOutlet var targetView: UIView!

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // Convert the point to the target view's coordinate system.
        // The target view isn't necessarily the immediate subview
        let pointForTargetView: CGPoint? = targetView?.convert(point, from: self)
        if (targetView?.bounds.contains(pointForTargetView!))! {
            // The target view may have its view hierarchy,
            // so call its hitTest method to return the right hit-test view
            return targetView?.hitTest(pointForTargetView ?? CGPoint.zero, with: event)
        }
        return super.hitTest(point, with: event)
    }
}

0

如果有人需要,这是快速的选择

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    if !self.clipsToBounds && !self.hidden && self.alpha > 0 {
        for subview in self.subviews.reverse() {
            let subPoint = subview.convertPoint(point, fromView:self);

            if let result = subview.hitTest(subPoint, withEvent:event) {
                return result;
            }
        }
    }

    return nil
}

0

将以下代码行放入视图层次结构:

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

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
    CGRect rect = self.bounds;
    BOOL isInside = CGRectContainsPoint(rect, point);
    if(!isInside)
    {
        for (UIView *view in self.subviews)
        {
            isInside = CGRectContainsPoint(view.frame, point);
            if(isInside)
                break;
        }
    }
    return isInside;
}

为了进一步澄清,在我的博客中对其进行了解释:“ goaheadwithiphonetech”中有关“自定义标注:按钮不可点击的问题”。

希望对您有帮助... !!!


您的博客已被删除,因此我们在哪里可以找到解释?
ishahak '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.