为什么 clipsToBounds 会阻止子视图被触摸?

Why does clipsToBounds prevent the subviews to be touched?

假设您有一个比其子视图更小的父视图。您将父视图的 clipsToBound 属性 设置为 false。如果点击子视图超出父视图边界的突出区域,为什么命中测试 return nil?

我的理解是命中测试从子视图开始,一直到父视图。那么为什么要晚于子视图进行测试的父视图的 属性 很重要呢?或者命中测试是否从根开始到树视图,如视图控制器 -> 主视图 -> 子视图?

我从 here 中找到了一个自定义命中测试,它允许您点击父视图边界之外的子视图区域,但我不确定为什么颠倒子视图的顺序一个区别(它有效,我只是不确定为什么)。我的例子甚至只有一个子视图。

class ViewController: UIViewController {
    let superview = CustomSuperview(frame: CGRect(origin: .zero, size: .init(width: 100, height: 100)))
    let subview = UIView(frame: CGRect(origin: .zero, size: .init(width: 200, height: 200)))

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.superview)
        self.superview.backgroundColor = .red
        self.superview.clipsToBounds = false
        
        self.superview.addSubview(self.subview)
        self.subview.backgroundColor = .blue
         
        let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
        self.subview.addGestureRecognizer(tap)
    }
    
    @objc func tapped(_ sender: UIGestureRecognizer) {
        print("tapped")
    }
}

class CustomSuperview: UIView {
    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
    }
}

My understanding is that the hit test starts from the subview and work its way up to the superview.

那你的理解就完全错误了。让我们解决这个问题。首先,让我们澄清一些其他的误解:

  • reversed完全是转移话题;它与它无关。子视图总是以相反的顺序进行测试,因为前面的子视图需要优先于后面的子视图。

  • clipsToBounds 也完全是转移注意力。它所做的只是改变您是否可以查看 其父视图之外的子视图;它对您是否可以触摸其父视图之外的子视图没有任何影响。

好的,这是如何工作的?让我们以包含子视图 A 的视图 V 为例。假设 A 在 V 之外。假设您可以看到 A,然后点击 A。

现在开始命中测试。但事情是这样的:它从 window 的级别开始,并按照视图层次结构 向下 的方式工作。所以 window 首先询问它的子视图;只有一个,视图控制器的主视图

所以现在我们递归,视图控制器的主视图询问它的个子视图。其中之一是 V。“嘿,V,水龙头在你体内吗?” “不!” (你必须同意这是正确答案,因为我们已经说过 A 在 V 之外。)

所以视图控制器的主视图放弃了 V,并且永远不会发现水龙头在A上,因为我们从来没有递归到那么远。所以它向链上报告:“水龙头不在我的子视图的 any 上,所以我必须报告水龙头在 me 上”点击 通过 进入视图控制器的主视图。

但是您可以通过覆盖 hitTest:

的实现来改变 那个行为
override func hitTest(_ point: CGPoint, with e: UIEvent?) -> UIView? {
    if let result = super.hitTest(point, with:e) {
        return result
    }
    for sub in self.subviews.reversed() {
        let pt = self.convert(point, to:sub)
        if let result = sub.hitTest(pt, with:e) {
            return result
        }
    }
    return nil
}