【问题标题】:How Do I Hit Test A SubView's Layers?如何点击测试子视图的图层?
【发布时间】:2019-05-05 00:17:02
【问题描述】:

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

操场(代码如下):

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

这是它的样子:

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

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

根视图被窃听@(188.5, 12.5)
命中测试返回“根视图 -> 根层”,帧为 (0.0, 0.0, 375.0, 668.0)

根视图被窃听@(188.5, 49.0)
命中测试返回“根视图 -> 根层”,帧为 (0.0, 0.0, 375.0, 668.0)

已点击根视图 @(188.5, 88.5)
命中测试返回“根视图 -> 根层”,帧为 (0.0, 0.0, 375.0, 668.0)

子视图点击@(140.0, 11.0)

子视图点击@(138.5, 32.5)

子视图点击@(138.5, 55.5)

子视图点击@(138.0, 84.0)

点击了子视图@(139.0, 113.0)
命中测试返回“子视图 -> 根层”,帧为 (50.0, 100.0, 275.0, 468.0)

点击了子视图@(138.0, 138.5)
命中测试返回“子视图 -> 根层”,帧为 (50.0, 100.0, 275.0, 468.0)

点击了子视图@(140.0, 171.5)
命中测试返回“子视图 -> 根层”,帧为 (50.0, 100.0, 275.0, 468.0)

点击了子视图@(137.5, 206.5)
命中测试返回“子视图 -> 子层”,帧 (50.0, 100.0, 175.0, 268.0)

点击了子视图@(135.5, 224.0)
命中测试返回“子视图 -> 子层”,帧 (50.0, 100.0, 175.0, 268.0)

您可以看到,当我在根视图中点击时,一切都是“正常的”。一旦我进入子视图,命中测试就不再找到图层,直到我低于子视图顶部 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)
命中测试返回“根视图 -> 根层”,帧为 (0.0, 0.0, 375.0, 668.0)

已点击根视图 @(187.0, 45.0)
命中测试返回“根视图 -> 根层”,帧为 (0.0, 0.0, 375.0, 668.0)

根视图被窃听@(187.0, 84.5)
命中测试返回“根视图 -> 根层”,帧为 (0.0, 0.0, 375.0, 668.0)

已点击子视图 @(138.0, 9.0)
命中测试返回“子视图 -> 根层”,帧为 (50.0, 100.0, 275.0, 468.0)

子视图已点击 @(137.5, 43.5)
命中测试返回“子视图 -> 根层”,帧为 (50.0, 100.0, 275.0, 468.0)

点击了子视图@(138.5, 77.5)
命中测试返回“子视图 -> 根层”,帧为 (50.0, 100.0, 275.0, 468.0)

点击了子视图@(138.0, 111.0)
命中测试返回“子视图 -> 子层 1”,帧为 (50.0, 100.0, 175.0, 268.0)

点击了子视图@(138.5, 140.5)
命中测试返回“子视图 -> 子层 1”,帧为 (50.0, 100.0, 175.0, 268.0)

点击了子视图@(139.5, 174.5)
命中测试返回“子视图 -> 子层 1”,帧为 (50.0, 100.0, 175.0, 268.0)

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

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

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).
        """
    }
}

【问题讨论】:

    标签: ios12 swift4.2 xcode10.1


    【解决方案1】:

    子视图根层的hitTest(:CGPoint)方法

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

    【讨论】:

      猜你喜欢
      • 2012-05-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多