我如何命中测试子视图的层?

How Do I Hit Test A SubView's Layers?

我在申请中遇到了挑战。我希望能够点击子视图并确定哪个子视图的图层被点击。我(天真地?)选择使用子视图根层的 hitTest(:CGPoint) 方法。结果不是我所期望的。我创建了一个游乐场来说明难度。

游乐场(代码如下):

  1. 创建视图控制器及其根视图
  2. 向根视图添加子视图
  3. 向子视图的根层添加一个子层

这是它的样子:

  1. 根视图的根层为黑色
  2. 子视图的根层为红色
  3. 子视图的子图层为半透明灰色
  4. 白线用于说明目的

在游乐场 运行 我开始敲击,从白线的顶部开始向下移动。以下是点击手势处理方法产生的打印语句:

Root view tapped @(188.5, 12.5)
Hit test returned "Root View -> Root Layer" with frame (0.0, 0.0, 375.0, 668.0)

Root view tapped @(188.5, 49.0)
Hit test returned "Root View -> Root Layer" with frame (0.0, 0.0, 375.0, 668.0)

Root view tapped @(188.5, 88.5)
Hit test returned "Root View -> Root Layer" with frame (0.0, 0.0, 375.0, 668.0)

Sub view tapped @(140.0, 11.0)

Sub view tapped @(138.5, 32.5)

Sub view tapped @(138.5, 55.5)

Sub view tapped @(138.0, 84.0)

Sub view tapped @(139.0, 113.0)
Hit test returned "Sub View -> Root Layer" with frame (50.0, 100.0, 275.0, 468.0)

Sub view tapped @(138.0, 138.5)
Hit test returned "Sub View -> Root Layer" with frame (50.0, 100.0, 275.0, 468.0)

Sub view tapped @(140.0, 171.5)
Hit test returned "Sub View -> Root Layer" with frame (50.0, 100.0, 275.0, 468.0)

Sub view tapped @(137.5, 206.5)
Hit test returned "Sub View -> Sub Layer" with frame (50.0, 100.0, 175.0, 268.0)

Sub view tapped @(135.5, 224.0)
Hit test returned "Sub View -> Sub Layer" with frame (50.0, 100.0, 175.0, 268.0)

当我点击根视图时,您可以看到 "normal"。一旦我进入子视图,命中测试就不再找到一个层,直到我低于子视图顶部 100 点以上;此时遇到子视图的根层。 100点后遇到子视图根层的子层

这是怎么回事?好吧,显然,当子视图被添加到根视图时,子视图的根层成为根视图根层的子层,并且它的框架被改变以反映它在根视图的根层而不是子视图中的位置。

所以,可能的问题是:

  1. 我对明显发生的事情是否正确?
  2. 是否正常且符合预期?
  3. 我该如何处理?

感谢您花时间考虑我的问题。

这是游乐场代码:

//: [Previous](@previous)

import UIKit
import PlaygroundSupport

class ViewController: UIViewController {

    class View : UIView {
        override func draw(_ rect: CGRect) {
            let context = UIGraphicsGetCurrentContext()!
            context.setStrokeColor(UIColor.white.cgColor)
            context.setLineWidth(2.0)
            context.move(to: CGPoint(x: rect.midX, y: 0))
            context.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
            context.strokePath()
        }
    }

    private var subview: View!

    override func loadView() {
        let rootView = View(frame: CGRect.zero)
        rootView.backgroundColor = .black
        rootView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized(_:))))
        rootView.layer.name = "Root View -> Root Layer"
        self.view = rootView
    }

    override func viewDidLayoutSubviews() {
        subview = View(frame: view.frame.insetBy(dx: 50.0, dy: 100.0))
        subview.backgroundColor = .red
        subview.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized(_:))))
        subview.layer.name = "Sub View -> Root Layer"

        let sublayer = CALayer()
        sublayer.bounds = subview.layer.bounds.insetBy(dx: 50, dy: 100)
        sublayer.position = CGPoint(x: subview.layer.bounds.midX, y: subview.layer.bounds.midY)
        sublayer.backgroundColor = UIColor.gray.cgColor.copy(alpha: 0.5)
        sublayer.name = "Sub View -> Sub Layer"
        subview.layer.addSublayer(sublayer)

        view.addSubview(subview)
    }

    @objc func tapGestureRecognized(_ sender: UITapGestureRecognizer) {
        guard sender.state == .ended, let tappedView = sender.view else { return }

        var viewName: String
        switch tappedView {
        case view: viewName = "Root"
        case subview: viewName = "Sub"
        default: return
        }

        let location = sender.location(in: tappedView)

        print("\n\(viewName) view tapped @\(location)")

        if let layer = tappedView.layer.hitTest(location) {
            print("Hit test returned <\(layer.name ?? "unknown")> with frame \(layer.frame)")
        }
    }
}

PlaygroundPage.current.liveView = ViewController()
PlaygroundPage.current.needsIndefiniteExecution = true

//: [Next](@next)

更新:问题已解决

在知识渊博的 Matt Nueburg(以及他最优秀的书)的指导下,我将点击手势处理程序方法修改为以下内容:

@objc func tapGestureRecognized(_ sender: UITapGestureRecognizer) {
    guard sender.state == .ended, let tappedView = sender.view else { return }

    var viewName: String
    let hitTestLocation: CGPoint

    switch tappedView {
    case view:
        viewName = "Root"
        hitTestLocation = sender.location(in: tappedView)
    case subview:
        viewName = "Sub"
        hitTestLocation = sender.location(in: tappedView.superview!)
   default: return
    }

    print("\n\(viewName) view tapped @\(sender.location(in: tappedView))")

    if let layer = tappedView.layer.hitTest(hitTestLocation) {
        print("Hit test returned <\(layer.name ?? "unknown")> with frame \(layer.frame)")
    }
}

现在控制台输出看起来不错:

点击根视图@(186.0, 19.0)
命中测试返回 "Root View -> Root Layer" 帧 (0.0, 0.0, 375.0, 668.0)

点击根视图@(187.0, 45.0)
命中测试返回 "Root View -> Root Layer" 帧 (0.0, 0.0, 375.0, 668.0)

点击根视图@(187.0, 84.5)
命中测试返回 "Root View -> Root Layer" 帧 (0.0, 0.0, 375.0, 668.0)

点击了子视图@(138.0, 9.0)
命中测试返回 "Sub View -> Root Layer" 帧 (50.0, 100.0, 275.0, 468.0)

点击了子视图@(137.5, 43.5)
命中测试返回 "Sub View -> Root Layer" 帧 (50.0, 100.0, 275.0, 468.0)

点击了子视图@(138.5, 77.5)
命中测试返回 "Sub View -> Root Layer" 帧 (50.0, 100.0, 275.0, 468.0)

点击了子视图@(138.0, 111.0)
命中测试返回 "Sub View -> Sub Layer 1" 帧 (50.0, 100.0, 175.0, 268.0)

点击了子视图@(138.5, 140.5)
命中测试返回 "Sub View -> Sub Layer 1" 帧 (50.0, 100.0, 175.0, 268.0)

点击了子视图@(139.5, 174.5)
命中测试返回 "Sub View -> Sub Layer 1" 帧 (50.0, 100.0, 175.0, 268.0)

第二次更新 - 更清晰的代码

我正在绕回我最终用来检测子层触摸的代码。我希望它比上面的 playground 代码更清晰。

private struct LayerTouched : CustomStringConvertible {

    let view: UIView // The touched view
    var layer: CALayer // The touched layer
    var location: CGPoint // The touch location expressed in the touched layer's coordinate system

    init(by recognizer: UIGestureRecognizer) {
        view = recognizer.view!
        let gestureLocation = recognizer.location(in: view)

        // AFAIK - Any touchable layer will have a super layer. Hence the forced unwrap is okay.
        let hitTestLocation = view.layer.superlayer!.convert(gestureLocation, from: view.layer)
        layer = view.layer.hitTest(hitTestLocation)!

        location = layer.convert(gestureLocation, from: view.layer)
    }

    var description: String {
        return """
        Touched \(layer)
        of \(view)
        at \(location).
        """
    }
}

the hitTest(:CGPoint) method of the subview's root layer

但是您忘记了层命中测试以一种特殊的方式工作。你提供的CGPoint必须在你命中测试层的superlayer的坐标系中。