【问题标题】:Why does clipsToBounds prevent the subviews to be touched?为什么 clipsToBounds 会阻止子视图被触摸?
【发布时间】:2020-12-05 20:56:24
【问题描述】:

假设您有一个尺寸小于其子视图的父视图。您将超级视图的clipsToBound 属性设置为false。如果点击子视图超出父视图边界的突出区域,为什么命中测试返回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
    }
}

【问题讨论】:

    标签: ios swift touch-event hittest


    【解决方案1】:

    我的理解是命中测试从子视图开始,一直到超级视图。

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

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

    • clipsToBounds 也完全是红鲱鱼。它所做的只是改变您是否可以看到其父视图之外的子视图;它对您是否可以触摸其父视图之外的子视图没有任何影响。

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

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

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

    所以视图控制器的主视图放弃 V,并且永远不会发现点击是在A上,因为我们从未递归到那么远。所以它报告了链条:“点击不在我的任何子视图上,所以我必须报告点击在上。”点击已穿过到视图控制器的主视图。

    但是您可以通过覆盖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
    }
    

    【讨论】:

    • 好的,我明白了。这就像树的深度优先遍历,首先测试超级视图,但是当涉及到同级时,最后一个被添加的首先被测试。
    • 颠倒的不是添加顺序,而是绘图顺序。子视图以从后到前的顺序出现,因为这就是我们希望它们绘制的方式。但我们希望它们按从前到后的顺序测试,因为这就是我们希望它们接触的方式。
    猜你喜欢
    • 1970-01-01
    • 2017-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多