如何在swift中打多个CAShapeLayer?
How to hit multiple CAShapeLayer in swift?
我有多个贝塞尔曲线路径,它们被合并到 CAShapeLayers 中,然后将所有图层添加到 UIImageView。我已经为 selection 的所有层实现了 hittest,但它 select 最后一个 CAShapeLayer。我想select其他层作为触摸,但我不知道如何?
这是我的触摸代码。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
super.touchesBegan(touches, with: event)
if let touch = touches.first, let touchedLayer = self.layerFor(touch)
{
print("hi")
selectedLayer = touchedLayer
touchedLayer.strokeColor = UIColor.red.cgColor
touchedLayer.lineWidth = CGFloat(3)
}
}
private func layerFor(_ touch: UITouch) -> CAShapeLayer?
{
let touchLocation = touch.location(in: self.backgroundIV)
let locationInView = self.backgroundIV!.convert(touchLocation, to: nil)
print("\(locationInView.x) \(locationInView.y)")
let hitPresentationLayer = view!.layer.presentation()?.hitTest(locationInView) as? CAShapeLayer
return hitPresentationLayer?.model()
}
这是我从路径创建图层的方法
fileprivate func createLayer(path: SVGBezierPath) -> CAShapeLayer {
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
if let any = path.svgAttributes["stroke"] {
shapeLayer.strokeColor = (any as! CGColor)
}
if let any = path.svgAttributes["fill"] {
shapeLayer.fillColor = (any as! CGColor)
}
return shapeLayer
}
编辑:
这是将形状层添加到父视图的代码
if let svgURL = Bundle.main.url(forResource: "image", withExtension: "svg") {
let paths = SVGBezierPath.pathsFromSVG(at: svgURL)
let scale = CGFloat(0.5)
for path in paths {
path.apply(CGAffineTransform(scaleX: scale, y: scale))
items.append(path)
let layer = createLayer(path: path)
layer.frame = self.backgroundIV.bounds
self.backgroundIV.layer.addSublayer(layer)
}
}
以及 touchBegan 方法的变化
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
let point = touches.first?.location(in: self.backgroundIV)
if let layer = self.backgroundIV.layer.hitTest(point!) as? CAShapeLayer {
selectedLayer = layer
selectedLayer.strokeColor = UIColor.red.cgColor
selectedLayer.lineWidth = CGFloat(3)
print("Touched")
}
}
我会在这里做一些假设...
- 您正在使用
PocketSVG
(或类似的)
- 你想要检测层
外的形状内的点击
即使您只寻找 层(不仅在层的路径内),我还是建议循环遍历子层并使用 .contains(point)
而不是尝试使用 layer.hitTest(point)
.
这是一个简单的例子:
import UIKit
import PocketSVG
enum DetectMode {
case All, TopMost, BottomMost
}
enum DetectType {
case ShapePath, ShapeBounds
}
class BoxesViewController: UIViewController {
var backgroundIV: UIImageView!
var detectMode: DetectMode = .All
var detectType: DetectType = .ShapePath
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
guard let svgURL = Bundle.main.url(forResource: "roundedboxes", withExtension: "svg") else {
fatalError("SVG file not found!!!")
}
backgroundIV = UIImageView()
let paths = SVGBezierPath.pathsFromSVG(at: svgURL)
let scale = CGFloat(0.5)
for path in paths {
path.apply(CGAffineTransform(scaleX: scale, y: scale))
//items.append(path)
let layer = createLayer(path: path)
self.backgroundIV.layer.addSublayer(layer)
}
let modeControl = UISegmentedControl(items: ["All", "Top Most", "Bottom Most"])
let typeControl = UISegmentedControl(items: ["Shape Path", "Shape Bounding Box"])
modeControl.translatesAutoresizingMaskIntoConstraints = false
typeControl.translatesAutoresizingMaskIntoConstraints = false
backgroundIV.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(modeControl)
view.addSubview(typeControl)
view.addSubview(backgroundIV)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
modeControl.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
modeControl.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
modeControl.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
typeControl.topAnchor.constraint(equalTo: modeControl.bottomAnchor, constant: 40.0),
typeControl.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
typeControl.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
backgroundIV.topAnchor.constraint(equalTo: typeControl.bottomAnchor, constant: 40.0),
backgroundIV.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
backgroundIV.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
backgroundIV.heightAnchor.constraint(equalTo: backgroundIV.widthAnchor),
])
modeControl.addTarget(self, action: #selector(modeChanged(_:)), for: .valueChanged)
typeControl.addTarget(self, action: #selector(typeChanged(_:)), for: .valueChanged)
modeControl.selectedSegmentIndex = 0
typeControl.selectedSegmentIndex = 0
// so we can see the frame of the image view
backgroundIV.backgroundColor = .white
}
@objc func modeChanged(_ sender: UISegmentedControl) -> Void {
switch sender.selectedSegmentIndex {
case 0:
detectMode = .All
case 1:
detectMode = .TopMost
case 2:
detectMode = .BottomMost
default:
()
}
}
@objc func typeChanged(_ sender: UISegmentedControl) -> Void {
switch sender.selectedSegmentIndex {
case 0:
detectType = .ShapePath
case 1:
detectType = .ShapeBounds
default:
()
}
}
fileprivate func createLayer(path: SVGBezierPath) -> CAShapeLayer {
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
if let any = path.svgAttributes["stroke"] {
shapeLayer.strokeColor = (any as! CGColor)
}
if let any = path.svgAttributes["fill"] {
shapeLayer.fillColor = (any as! CGColor)
}
return shapeLayer
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: self.backgroundIV),
// make sure backgroundIV has sublayers
let layers = self.backgroundIV.layer.sublayers
else { return }
var hitLayers: [CAShapeLayer] = []
// loop through all sublayers
for subLayer in layers {
// make sure
// it is a CAShapeLayer
// it has a path
if let thisLayer = subLayer as? CAShapeLayer,
let pth = thisLayer.path {
// clear the lineWidth... we'll reset it after getting the hit layers
thisLayer.lineWidth = 0
// convert touch point from backgroundIV.layer to thisLayer
let layerPoint: CGPoint = thisLayer.convert(point, from: self.backgroundIV.layer)
if detectType == .ShapePath {
// does the path contain the point?
if pth.contains(layerPoint) {
hitLayers.append(thisLayer)
}
} else if detectType == .ShapeBounds {
if pth.boundingBox.contains(layerPoint) {
hitLayers.append(thisLayer)
}
}
}
}
if detectMode == .All {
hitLayers.forEach { layer in
layer.strokeColor = UIColor.cyan.cgColor
layer.lineWidth = 3
}
} else if detectMode == .TopMost {
if let layer = hitLayers.last {
layer.strokeColor = UIColor.cyan.cgColor
layer.lineWidth = 3
}
} else if detectMode == .BottomMost {
if let layer = hitLayers.first {
layer.strokeColor = UIColor.cyan.cgColor
layer.lineWidth = 3
}
}
}
}
当你运行这个时,它看起来像这样(我在导航控制器中):
这是我使用的 SVG 文件:
roundedboxes.svg
<?xml version="1.0" encoding="UTF-8"?>
<svg width="240px" height="240px" viewBox="0 0 240 240" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Untitled</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="RedRectangle" fill-opacity="0.75" fill="#FF0000" x="0" y="0" width="160" height="160" rx="60"></rect>
<rect id="GreenRectangle" fill-opacity="0.75" fill="#00FF00" x="80" y="0" width="160" height="160" rx="60"></rect>
<rect id="BlueRectangle" fill-opacity="0.75" fill="#0000FF" x="40" y="80" width="160" height="160" rx="60"></rect>
</g>
</svg>
默认情况下,我们将在每个层上测试形状路径内的点击,我们将突出显示所有通过测试的层:
请注意,在形状/层重叠的地方轻敲会 select 轻敲在其路径内的所有层。
如果我们只想要 Top Most 层,我们将得到:
如果我们只想要 最底层 层,我们将得到:
如果我们切换到检测 Shape Bounding Box,我们会得到:
如果这至少接近您的目标,请试用此示例代码,看看您得到了什么。
我有多个贝塞尔曲线路径,它们被合并到 CAShapeLayers 中,然后将所有图层添加到 UIImageView。我已经为 selection 的所有层实现了 hittest,但它 select 最后一个 CAShapeLayer。我想select其他层作为触摸,但我不知道如何?
这是我的触摸代码。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
super.touchesBegan(touches, with: event)
if let touch = touches.first, let touchedLayer = self.layerFor(touch)
{
print("hi")
selectedLayer = touchedLayer
touchedLayer.strokeColor = UIColor.red.cgColor
touchedLayer.lineWidth = CGFloat(3)
}
}
private func layerFor(_ touch: UITouch) -> CAShapeLayer?
{
let touchLocation = touch.location(in: self.backgroundIV)
let locationInView = self.backgroundIV!.convert(touchLocation, to: nil)
print("\(locationInView.x) \(locationInView.y)")
let hitPresentationLayer = view!.layer.presentation()?.hitTest(locationInView) as? CAShapeLayer
return hitPresentationLayer?.model()
}
这是我从路径创建图层的方法
fileprivate func createLayer(path: SVGBezierPath) -> CAShapeLayer {
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
if let any = path.svgAttributes["stroke"] {
shapeLayer.strokeColor = (any as! CGColor)
}
if let any = path.svgAttributes["fill"] {
shapeLayer.fillColor = (any as! CGColor)
}
return shapeLayer
}
编辑: 这是将形状层添加到父视图的代码
if let svgURL = Bundle.main.url(forResource: "image", withExtension: "svg") {
let paths = SVGBezierPath.pathsFromSVG(at: svgURL)
let scale = CGFloat(0.5)
for path in paths {
path.apply(CGAffineTransform(scaleX: scale, y: scale))
items.append(path)
let layer = createLayer(path: path)
layer.frame = self.backgroundIV.bounds
self.backgroundIV.layer.addSublayer(layer)
}
}
以及 touchBegan 方法的变化
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
let point = touches.first?.location(in: self.backgroundIV)
if let layer = self.backgroundIV.layer.hitTest(point!) as? CAShapeLayer {
selectedLayer = layer
selectedLayer.strokeColor = UIColor.red.cgColor
selectedLayer.lineWidth = CGFloat(3)
print("Touched")
}
}
我会在这里做一些假设...
- 您正在使用
PocketSVG
(或类似的) - 你想要检测层 外的形状内的点击
即使您只寻找 层(不仅在层的路径内),我还是建议循环遍历子层并使用 .contains(point)
而不是尝试使用 layer.hitTest(point)
.
这是一个简单的例子:
import UIKit
import PocketSVG
enum DetectMode {
case All, TopMost, BottomMost
}
enum DetectType {
case ShapePath, ShapeBounds
}
class BoxesViewController: UIViewController {
var backgroundIV: UIImageView!
var detectMode: DetectMode = .All
var detectType: DetectType = .ShapePath
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
guard let svgURL = Bundle.main.url(forResource: "roundedboxes", withExtension: "svg") else {
fatalError("SVG file not found!!!")
}
backgroundIV = UIImageView()
let paths = SVGBezierPath.pathsFromSVG(at: svgURL)
let scale = CGFloat(0.5)
for path in paths {
path.apply(CGAffineTransform(scaleX: scale, y: scale))
//items.append(path)
let layer = createLayer(path: path)
self.backgroundIV.layer.addSublayer(layer)
}
let modeControl = UISegmentedControl(items: ["All", "Top Most", "Bottom Most"])
let typeControl = UISegmentedControl(items: ["Shape Path", "Shape Bounding Box"])
modeControl.translatesAutoresizingMaskIntoConstraints = false
typeControl.translatesAutoresizingMaskIntoConstraints = false
backgroundIV.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(modeControl)
view.addSubview(typeControl)
view.addSubview(backgroundIV)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
modeControl.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
modeControl.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
modeControl.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
typeControl.topAnchor.constraint(equalTo: modeControl.bottomAnchor, constant: 40.0),
typeControl.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
typeControl.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
backgroundIV.topAnchor.constraint(equalTo: typeControl.bottomAnchor, constant: 40.0),
backgroundIV.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
backgroundIV.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
backgroundIV.heightAnchor.constraint(equalTo: backgroundIV.widthAnchor),
])
modeControl.addTarget(self, action: #selector(modeChanged(_:)), for: .valueChanged)
typeControl.addTarget(self, action: #selector(typeChanged(_:)), for: .valueChanged)
modeControl.selectedSegmentIndex = 0
typeControl.selectedSegmentIndex = 0
// so we can see the frame of the image view
backgroundIV.backgroundColor = .white
}
@objc func modeChanged(_ sender: UISegmentedControl) -> Void {
switch sender.selectedSegmentIndex {
case 0:
detectMode = .All
case 1:
detectMode = .TopMost
case 2:
detectMode = .BottomMost
default:
()
}
}
@objc func typeChanged(_ sender: UISegmentedControl) -> Void {
switch sender.selectedSegmentIndex {
case 0:
detectType = .ShapePath
case 1:
detectType = .ShapeBounds
default:
()
}
}
fileprivate func createLayer(path: SVGBezierPath) -> CAShapeLayer {
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
if let any = path.svgAttributes["stroke"] {
shapeLayer.strokeColor = (any as! CGColor)
}
if let any = path.svgAttributes["fill"] {
shapeLayer.fillColor = (any as! CGColor)
}
return shapeLayer
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: self.backgroundIV),
// make sure backgroundIV has sublayers
let layers = self.backgroundIV.layer.sublayers
else { return }
var hitLayers: [CAShapeLayer] = []
// loop through all sublayers
for subLayer in layers {
// make sure
// it is a CAShapeLayer
// it has a path
if let thisLayer = subLayer as? CAShapeLayer,
let pth = thisLayer.path {
// clear the lineWidth... we'll reset it after getting the hit layers
thisLayer.lineWidth = 0
// convert touch point from backgroundIV.layer to thisLayer
let layerPoint: CGPoint = thisLayer.convert(point, from: self.backgroundIV.layer)
if detectType == .ShapePath {
// does the path contain the point?
if pth.contains(layerPoint) {
hitLayers.append(thisLayer)
}
} else if detectType == .ShapeBounds {
if pth.boundingBox.contains(layerPoint) {
hitLayers.append(thisLayer)
}
}
}
}
if detectMode == .All {
hitLayers.forEach { layer in
layer.strokeColor = UIColor.cyan.cgColor
layer.lineWidth = 3
}
} else if detectMode == .TopMost {
if let layer = hitLayers.last {
layer.strokeColor = UIColor.cyan.cgColor
layer.lineWidth = 3
}
} else if detectMode == .BottomMost {
if let layer = hitLayers.first {
layer.strokeColor = UIColor.cyan.cgColor
layer.lineWidth = 3
}
}
}
}
当你运行这个时,它看起来像这样(我在导航控制器中):
这是我使用的 SVG 文件:
roundedboxes.svg
<?xml version="1.0" encoding="UTF-8"?>
<svg width="240px" height="240px" viewBox="0 0 240 240" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Untitled</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="RedRectangle" fill-opacity="0.75" fill="#FF0000" x="0" y="0" width="160" height="160" rx="60"></rect>
<rect id="GreenRectangle" fill-opacity="0.75" fill="#00FF00" x="80" y="0" width="160" height="160" rx="60"></rect>
<rect id="BlueRectangle" fill-opacity="0.75" fill="#0000FF" x="40" y="80" width="160" height="160" rx="60"></rect>
</g>
</svg>
默认情况下,我们将在每个层上测试形状路径内的点击,我们将突出显示所有通过测试的层:
请注意,在形状/层重叠的地方轻敲会 select 轻敲在其路径内的所有层。
如果我们只想要 Top Most 层,我们将得到:
如果我们只想要 最底层 层,我们将得到:
如果我们切换到检测 Shape Bounding Box,我们会得到:
如果这至少接近您的目标,请试用此示例代码,看看您得到了什么。