如何在核心图形中进行命中检测
How to do hit detection in core graphics
核心图形对我来说很新,我在检测自定义图形上的点击时遇到了一些问题。
我用 paincode 的演示生成了 som 代码,然后我对其进行了大量修改。它画了一个像这样的“馅饼”:
我为此使用的代码如下所示:
import UIKit
public class DrawTest : NSObject {
static var hitAreas = [Int:UIBezierPath]()
static func didHit(_ point: CGPoint){
let res = hitAreas.first{ [=10=].value.contains(point) }?.key
print("HIT: ", res)
}
public class func drawDartboard(frame targetFrame: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let resizedFrame: CGRect = targetFrame
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100)
let sliceRect = CGRect(x: 0, y: 0, width: 100, height: 100)
context.saveGState()
context.clip(to: sliceRect)
context.translateBy(x: sliceRect.minX, y: sliceRect.minY)
context.translateBy(x: 0, y: sliceRect.height)
context.scaleBy(x: 1, y: -1)
let dark = UIColor(red: 0.235, green: 0.208, blue: 0.208, alpha: 1.000)
let light = UIColor(red: 0.435, green: 0.408, blue: 0.408, alpha: 1.000)
var slice = 0
while slice < 20 {
let sliceColor = slice%2 == 0 ? dark : light
DrawTest.drawSlice(frame: CGRect(origin: .zero, size: sliceRect.size), roration: CGFloat(slice*18), sliceColor: sliceColor, slice: slice )
slice += 1
}
context.restoreGState()
}
public class func drawSlice(frame targetFrame: CGRect, roration: CGFloat, sliceColor: UIColor, slice: Int) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let resizedFrame: CGRect = targetFrame
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100)
context.saveGState()
context.translateBy(x: 49.99, y: 50)
context.rotate(by: roration * CGFloat.pi/180)
let sliceFillPath = UIBezierPath()
sliceFillPath.move(to: CGPoint(x: -7.82, y: 49.38))
sliceFillPath.addCurve(to: CGPoint(x: 7.83, y: 49.38), controlPoint1: CGPoint(x: -2.63, y: 50.2), controlPoint2: CGPoint(x: 2.65, y: 50.2))
sliceFillPath.addLine(to: CGPoint(x: 0.01, y: -0))
sliceFillPath.addLine(to: CGPoint(x: -7.82, y: 49.38))
sliceFillPath.close()
sliceColor.setFill()
sliceFillPath.fill()
hitAreas[slice] = sliceFillPath
context.restoreGState()
}
}
我正在从一个简单的 UIView 子类调用绘图代码,如下所示。这也是我附加了 TapGerstureRecognizer。
import UIKit
class DartBoardView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
@objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
DrawTest.didHit(loc)
}
}
override func draw(_ rect: CGRect) {
DrawTest.drawDartboard(frame: bounds)
}
}
绘图看起来像我想要的,但我希望能够 select 每个切片,这是不起作用的部分。我很确定这个问题与我传递给 didHit
的点有关,它是我的视图的本地点,但是我存储在 hitAreas 中并调用 contains
的 UIBezierPath 使用 UIBezierPath 的本地坐标,这是为什么我从来没有受到打击。
我不知道如何解决这个问题,迫切需要帮助。我的猜测是,这应该通过 1) 在 UIView 的坐标系上直接绘制我的切片来解决,但这需要大量的数学运算 2) 在命中测试时以某种方式将每个 UIBezierPath 的局部坐标转换为视图的范围
这一切都非常令人困惑,非常感谢建设性的意见。
有多种方法,具体取决于您的最终目标。
一种方法:
- 计算“每切片的度数”... 360 / 20 = 18
- 获取中心点到触摸点的角度
- 将角度“固定”为切片宽度的 1/2(因为切片不从零开始)
- 将该角度除以每切片的度数以获得切片数
使用这两个扩展可以轻松获得角度(以度为单位):
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
extension CGPoint {
func angle(to otherPoint: CGPoint) -> CGFloat {
let pX = otherPoint.x - x
let pY = otherPoint.y - y
let radians = atan2f(Float(pY), Float(pX))
var degrees = CGFloat(radians).degrees
while degrees < 0 {
degrees += 360
}
return degrees
}
}
并且,在您发布的代码中,在您的 DrawTest
class 中,将 didHit
更改为:
static func didHit(_ point: CGPoint, in bounds: CGRect){
let c: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let angle = c.angle(to: point)
var fixedAngle = Int(angle) + 99 // 90 degrees + 1/2 of slice width
if fixedAngle >= 360 {
fixedAngle -= 360
}
print("HIT:", fixedAngle / 18)
}
并在您从 DartBoardView
class 调用它时包含边界,如:
@objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
// include self's bounds
DrawTest.didHit(loc, in: bounds)
}
}
缺点包括:
- 您还需要检查“线长”以确保它不会延伸到圆外
- 您无法轻松访问切片贝塞尔曲线路径(如果您想对它们执行其他操作)
另一种方法是为每个切片使用形状图层,从而更容易跟踪贝塞尔曲线路径。
从切片的 Struct
开始:
struct Slice {
var color: UIColor = .white
var path: UIBezierPath = UIBezierPath()
var shapeLayer: CAShapeLayer = CAShapeLayer()
var key: Int = 0
}
DartBoardView
class 变为(注意:它使用与上面相同的 CGFloat
扩展名):
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
class DartBoardView: UIView {
// array of slices
var slices: [Slice] = []
// slice width in degrees
let sliceWidth: CGFloat = 360.0 / 20.0
// easy to understand 12 o'clock (3 o'clock is Zero)
let twelveOClock: CGFloat = 270
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let dark = UIColor(red: 0.235, green: 0.208, blue: 0.208, alpha: 1.000)
let light = UIColor(red: 0.435, green: 0.408, blue: 0.408, alpha: 1.000)
for slice in 0..<20 {
let sliceColor = slice % 2 == 1 ? dark : light
let s = Slice(color: sliceColor, key: slice)
s.shapeLayer.fillColor = s.color.cgColor
layer.addSublayer(s.shapeLayer)
slices.append(s)
}
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
@objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
if let s = slices.first(where: { [=14=].path.contains(loc) }) {
print("HIT:", s.key)
} else {
print("Tapped outside the circle!")
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
let c: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let radius: CGFloat = bounds.midX
// slice width in radians
let ww: CGFloat = sliceWidth.radians
// start 1/2 sliceWidth less than 12 o'clock
var startDegrees: CGFloat = twelveOClock.radians - (ww * 0.5)
for i in 0..<slices.count {
let endDegrees: CGFloat = startDegrees + ww
let pth: UIBezierPath = UIBezierPath()
pth.addArc(withCenter: c, radius: radius, startAngle: startDegrees, endAngle: endDegrees, clockwise: true)
pth.addLine(to: c)
pth.close()
slices[i].path = pth
slices[i].shapeLayer.path = pth.cgPath
startDegrees = endDegrees
}
}
}
这里有一个示例控制器 class 来演示:
class DartBoardViewController: UIViewController {
let dartBoard = DartBoardView()
override func viewDidLoad() {
super.viewDidLoad()
dartBoard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dartBoard)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
dartBoard.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dartBoard.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dartBoard.heightAnchor.constraint(equalTo: dartBoard.widthAnchor),
dartBoard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
dartBoard.backgroundColor = .black
}
}
编辑
并不像看起来那么复杂。
这是一个完整的 Dart Board 的实现(没有数字 - 我将把它作为练习留给你):
段结构
struct Segment {
var value: Int = 0
var multiplier: Int = 1
var color: UIColor = .cyan
var path: UIBezierPath = UIBezierPath()
var layer: CAShapeLayer = CAShapeLayer()
}
DartBoardView class
class DartBoardView: UIView {
var doubleSegments: [Segment] = [Segment]()
var outerSingleSegments: [Segment] = [Segment]()
var tripleSegments: [Segment] = [Segment]()
var innerSingleSegments: [Segment] = [Segment]()
var singleBullSegment: Segment = Segment()
var doubleBullSegment: Segment = Segment()
var allSegments: [Segment] = [Segment]()
let boardLayer: CAShapeLayer = CAShapeLayer()
let darkColor: UIColor = UIColor(white: 0.1, alpha: 1.0)
let lightColor: UIColor = UIColor(red: 0.975, green: 0.9, blue: 0.8, alpha: 1.0)
let darkRedColor: UIColor = UIColor(red: 0.8, green: 0.1, blue: 0.1, alpha: 1.0)
let darkGreenColor: UIColor = UIColor(red: 0.0, green: 0.5, blue: 0.3, alpha: 1.0)
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(boardLayer)
boardLayer.fillColor = UIColor.black.cgColor
// points starting at 3 o'clock
let values: [Int] = [
6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5, 20, 1, 18, 4, 13,
]
// local vars for reuse
var seg: Segment = Segment()
var c: UIColor = .white
// doubles and triples
for i in 0..<values.count {
c = i % 2 == 1 ? darkRedColor : darkGreenColor
seg = Segment(value: values[i],
multiplier: 2,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
doubleSegments.append(seg)
seg = Segment(value: values[i],
multiplier: 3,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
tripleSegments.append(seg)
}
// singles
for i in 0..<values.count {
c = i % 2 == 1 ? darkColor : lightColor
seg = Segment(value: values[i],
multiplier: 1,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
outerSingleSegments.append(seg)
seg = Segment(value: values[i],
multiplier: 1,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
innerSingleSegments.append(seg)
}
// bull and double bull
seg = Segment(value: 25,
multiplier: 1,
color: darkGreenColor,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
singleBullSegment = seg
seg = Segment(value: 25,
multiplier: 2,
color: darkRedColor,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
doubleBullSegment = seg
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
@objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
if let s = allSegments.first(where: { [=17=].path.contains(loc) }) {
print("HIT:", s.multiplier == 3 ? "Triple" : s.multiplier == 2 ? "Double" : "Single", s.value)
} else {
print("Tapped outside!")
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
// initialize local variables for reuse / readability
var startAngle: CGFloat = 0
var outerDoubleRadius: CGFloat = 0.0
var innerDoubleRadius: CGFloat = 0.0
var outerTripleRadius: CGFloat = 0.0
var innerTripleRadius: CGFloat = 0.0
var outerBullRadius: CGFloat = 0.0
var innerBullRadius: CGFloat = 0.0
// initialize local constants
let viewCenter: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
// leave 20% for the numbers area
let diameter = bounds.width * 0.8
// dart board radii in mm
let specRadii: [CGFloat] = [
170, 162, 107, 99, 16, 6
]
// convert to view size
let factor: CGFloat = (diameter * 0.5) / specRadii[0]
outerDoubleRadius = specRadii[0] * factor
innerDoubleRadius = specRadii[1] * factor
outerTripleRadius = specRadii[2] * factor
innerTripleRadius = specRadii[3] * factor
outerBullRadius = specRadii[4] * factor
innerBullRadius = specRadii[5] * factor
let wireColor: UIColor = UIColor(white: 0.8, alpha: 1.0)
let wedgeWidth: CGFloat = 360.0 / 20.0
let incAngle: CGFloat = wedgeWidth.radians
startAngle = -(incAngle * 0.5)
var path: UIBezierPath = UIBezierPath()
// outer board layer
path = UIBezierPath(ovalIn: bounds)
boardLayer.path = path.cgPath
for i in 0..<20 {
let endAngle = startAngle + incAngle
var shape = doubleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerDoubleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: innerDoubleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
doubleSegments[i].path = path
shape.fillColor = doubleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = outerSingleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: innerDoubleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: outerTripleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
outerSingleSegments[i].path = path
shape.fillColor = outerSingleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = tripleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerTripleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: innerTripleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
tripleSegments[i].path = path
shape.fillColor = tripleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = innerSingleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: innerTripleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: outerBullRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
innerSingleSegments[i].path = path
shape.fillColor = innerSingleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
startAngle = endAngle
}
let singleBullPath = UIBezierPath(ovalIn: CGRect(x: viewCenter.x - outerBullRadius, y: viewCenter.y - outerBullRadius, width: outerBullRadius * 2, height: outerBullRadius * 2))
let doubleBullPath = UIBezierPath(ovalIn: CGRect(x: viewCenter.x - innerBullRadius, y: viewCenter.y - innerBullRadius, width: innerBullRadius * 2, height: innerBullRadius * 2))
var shape = singleBullSegment.layer
singleBullPath.append(doubleBullPath)
singleBullPath.usesEvenOddFillRule = true
shape.fillRule = .evenOdd
shape.path = singleBullPath.cgPath
singleBullSegment.path = singleBullPath
shape.fillColor = singleBullSegment.color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = doubleBullSegment.layer
shape.path = doubleBullPath.cgPath
doubleBullSegment.path = doubleBullPath
shape.fillColor = doubleBullSegment.color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
// append all segments for hit-testing
allSegments = []
allSegments.append(contentsOf: tripleSegments)
allSegments.append(contentsOf: outerSingleSegments)
allSegments.append(contentsOf: doubleSegments)
allSegments.append(contentsOf: innerSingleSegments)
allSegments.append(singleBullSegment)
allSegments.append(doubleBullSegment)
}
}
CGFloat 扩展
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
示例视图控制器
class DartBoardViewController: UIViewController {
let dartBoard = DartBoardView()
override func viewDidLoad() {
super.viewDidLoad()
dartBoard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dartBoard)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
dartBoard.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dartBoard.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dartBoard.heightAnchor.constraint(equalTo: dartBoard.widthAnchor),
dartBoard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
dartBoard.backgroundColor = .clear
}
}
结果:
点击几下即可调试输出:
HIT: Double 20
HIT: Single 18
HIT: Triple 2
HIT: Single 25
HIT: Double 25
核心图形对我来说很新,我在检测自定义图形上的点击时遇到了一些问题。
我用 paincode 的演示生成了 som 代码,然后我对其进行了大量修改。它画了一个像这样的“馅饼”:
我为此使用的代码如下所示:
import UIKit
public class DrawTest : NSObject {
static var hitAreas = [Int:UIBezierPath]()
static func didHit(_ point: CGPoint){
let res = hitAreas.first{ [=10=].value.contains(point) }?.key
print("HIT: ", res)
}
public class func drawDartboard(frame targetFrame: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let resizedFrame: CGRect = targetFrame
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100)
let sliceRect = CGRect(x: 0, y: 0, width: 100, height: 100)
context.saveGState()
context.clip(to: sliceRect)
context.translateBy(x: sliceRect.minX, y: sliceRect.minY)
context.translateBy(x: 0, y: sliceRect.height)
context.scaleBy(x: 1, y: -1)
let dark = UIColor(red: 0.235, green: 0.208, blue: 0.208, alpha: 1.000)
let light = UIColor(red: 0.435, green: 0.408, blue: 0.408, alpha: 1.000)
var slice = 0
while slice < 20 {
let sliceColor = slice%2 == 0 ? dark : light
DrawTest.drawSlice(frame: CGRect(origin: .zero, size: sliceRect.size), roration: CGFloat(slice*18), sliceColor: sliceColor, slice: slice )
slice += 1
}
context.restoreGState()
}
public class func drawSlice(frame targetFrame: CGRect, roration: CGFloat, sliceColor: UIColor, slice: Int) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let resizedFrame: CGRect = targetFrame
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100)
context.saveGState()
context.translateBy(x: 49.99, y: 50)
context.rotate(by: roration * CGFloat.pi/180)
let sliceFillPath = UIBezierPath()
sliceFillPath.move(to: CGPoint(x: -7.82, y: 49.38))
sliceFillPath.addCurve(to: CGPoint(x: 7.83, y: 49.38), controlPoint1: CGPoint(x: -2.63, y: 50.2), controlPoint2: CGPoint(x: 2.65, y: 50.2))
sliceFillPath.addLine(to: CGPoint(x: 0.01, y: -0))
sliceFillPath.addLine(to: CGPoint(x: -7.82, y: 49.38))
sliceFillPath.close()
sliceColor.setFill()
sliceFillPath.fill()
hitAreas[slice] = sliceFillPath
context.restoreGState()
}
}
我正在从一个简单的 UIView 子类调用绘图代码,如下所示。这也是我附加了 TapGerstureRecognizer。
import UIKit
class DartBoardView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
@objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
DrawTest.didHit(loc)
}
}
override func draw(_ rect: CGRect) {
DrawTest.drawDartboard(frame: bounds)
}
}
绘图看起来像我想要的,但我希望能够 select 每个切片,这是不起作用的部分。我很确定这个问题与我传递给 didHit
的点有关,它是我的视图的本地点,但是我存储在 hitAreas 中并调用 contains
的 UIBezierPath 使用 UIBezierPath 的本地坐标,这是为什么我从来没有受到打击。
我不知道如何解决这个问题,迫切需要帮助。我的猜测是,这应该通过 1) 在 UIView 的坐标系上直接绘制我的切片来解决,但这需要大量的数学运算 2) 在命中测试时以某种方式将每个 UIBezierPath 的局部坐标转换为视图的范围
这一切都非常令人困惑,非常感谢建设性的意见。
有多种方法,具体取决于您的最终目标。
一种方法:
- 计算“每切片的度数”... 360 / 20 = 18
- 获取中心点到触摸点的角度
- 将角度“固定”为切片宽度的 1/2(因为切片不从零开始)
- 将该角度除以每切片的度数以获得切片数
使用这两个扩展可以轻松获得角度(以度为单位):
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
extension CGPoint {
func angle(to otherPoint: CGPoint) -> CGFloat {
let pX = otherPoint.x - x
let pY = otherPoint.y - y
let radians = atan2f(Float(pY), Float(pX))
var degrees = CGFloat(radians).degrees
while degrees < 0 {
degrees += 360
}
return degrees
}
}
并且,在您发布的代码中,在您的 DrawTest
class 中,将 didHit
更改为:
static func didHit(_ point: CGPoint, in bounds: CGRect){
let c: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let angle = c.angle(to: point)
var fixedAngle = Int(angle) + 99 // 90 degrees + 1/2 of slice width
if fixedAngle >= 360 {
fixedAngle -= 360
}
print("HIT:", fixedAngle / 18)
}
并在您从 DartBoardView
class 调用它时包含边界,如:
@objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
// include self's bounds
DrawTest.didHit(loc, in: bounds)
}
}
缺点包括:
- 您还需要检查“线长”以确保它不会延伸到圆外
- 您无法轻松访问切片贝塞尔曲线路径(如果您想对它们执行其他操作)
另一种方法是为每个切片使用形状图层,从而更容易跟踪贝塞尔曲线路径。
从切片的 Struct
开始:
struct Slice {
var color: UIColor = .white
var path: UIBezierPath = UIBezierPath()
var shapeLayer: CAShapeLayer = CAShapeLayer()
var key: Int = 0
}
DartBoardView
class 变为(注意:它使用与上面相同的 CGFloat
扩展名):
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
class DartBoardView: UIView {
// array of slices
var slices: [Slice] = []
// slice width in degrees
let sliceWidth: CGFloat = 360.0 / 20.0
// easy to understand 12 o'clock (3 o'clock is Zero)
let twelveOClock: CGFloat = 270
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let dark = UIColor(red: 0.235, green: 0.208, blue: 0.208, alpha: 1.000)
let light = UIColor(red: 0.435, green: 0.408, blue: 0.408, alpha: 1.000)
for slice in 0..<20 {
let sliceColor = slice % 2 == 1 ? dark : light
let s = Slice(color: sliceColor, key: slice)
s.shapeLayer.fillColor = s.color.cgColor
layer.addSublayer(s.shapeLayer)
slices.append(s)
}
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
@objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
if let s = slices.first(where: { [=14=].path.contains(loc) }) {
print("HIT:", s.key)
} else {
print("Tapped outside the circle!")
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
let c: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let radius: CGFloat = bounds.midX
// slice width in radians
let ww: CGFloat = sliceWidth.radians
// start 1/2 sliceWidth less than 12 o'clock
var startDegrees: CGFloat = twelveOClock.radians - (ww * 0.5)
for i in 0..<slices.count {
let endDegrees: CGFloat = startDegrees + ww
let pth: UIBezierPath = UIBezierPath()
pth.addArc(withCenter: c, radius: radius, startAngle: startDegrees, endAngle: endDegrees, clockwise: true)
pth.addLine(to: c)
pth.close()
slices[i].path = pth
slices[i].shapeLayer.path = pth.cgPath
startDegrees = endDegrees
}
}
}
这里有一个示例控制器 class 来演示:
class DartBoardViewController: UIViewController {
let dartBoard = DartBoardView()
override func viewDidLoad() {
super.viewDidLoad()
dartBoard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dartBoard)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
dartBoard.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dartBoard.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dartBoard.heightAnchor.constraint(equalTo: dartBoard.widthAnchor),
dartBoard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
dartBoard.backgroundColor = .black
}
}
编辑
并不像看起来那么复杂。
这是一个完整的 Dart Board 的实现(没有数字 - 我将把它作为练习留给你):
段结构
struct Segment {
var value: Int = 0
var multiplier: Int = 1
var color: UIColor = .cyan
var path: UIBezierPath = UIBezierPath()
var layer: CAShapeLayer = CAShapeLayer()
}
DartBoardView class
class DartBoardView: UIView {
var doubleSegments: [Segment] = [Segment]()
var outerSingleSegments: [Segment] = [Segment]()
var tripleSegments: [Segment] = [Segment]()
var innerSingleSegments: [Segment] = [Segment]()
var singleBullSegment: Segment = Segment()
var doubleBullSegment: Segment = Segment()
var allSegments: [Segment] = [Segment]()
let boardLayer: CAShapeLayer = CAShapeLayer()
let darkColor: UIColor = UIColor(white: 0.1, alpha: 1.0)
let lightColor: UIColor = UIColor(red: 0.975, green: 0.9, blue: 0.8, alpha: 1.0)
let darkRedColor: UIColor = UIColor(red: 0.8, green: 0.1, blue: 0.1, alpha: 1.0)
let darkGreenColor: UIColor = UIColor(red: 0.0, green: 0.5, blue: 0.3, alpha: 1.0)
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(boardLayer)
boardLayer.fillColor = UIColor.black.cgColor
// points starting at 3 o'clock
let values: [Int] = [
6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5, 20, 1, 18, 4, 13,
]
// local vars for reuse
var seg: Segment = Segment()
var c: UIColor = .white
// doubles and triples
for i in 0..<values.count {
c = i % 2 == 1 ? darkRedColor : darkGreenColor
seg = Segment(value: values[i],
multiplier: 2,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
doubleSegments.append(seg)
seg = Segment(value: values[i],
multiplier: 3,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
tripleSegments.append(seg)
}
// singles
for i in 0..<values.count {
c = i % 2 == 1 ? darkColor : lightColor
seg = Segment(value: values[i],
multiplier: 1,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
outerSingleSegments.append(seg)
seg = Segment(value: values[i],
multiplier: 1,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
innerSingleSegments.append(seg)
}
// bull and double bull
seg = Segment(value: 25,
multiplier: 1,
color: darkGreenColor,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
singleBullSegment = seg
seg = Segment(value: 25,
multiplier: 2,
color: darkRedColor,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
doubleBullSegment = seg
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
@objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
if let s = allSegments.first(where: { [=17=].path.contains(loc) }) {
print("HIT:", s.multiplier == 3 ? "Triple" : s.multiplier == 2 ? "Double" : "Single", s.value)
} else {
print("Tapped outside!")
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
// initialize local variables for reuse / readability
var startAngle: CGFloat = 0
var outerDoubleRadius: CGFloat = 0.0
var innerDoubleRadius: CGFloat = 0.0
var outerTripleRadius: CGFloat = 0.0
var innerTripleRadius: CGFloat = 0.0
var outerBullRadius: CGFloat = 0.0
var innerBullRadius: CGFloat = 0.0
// initialize local constants
let viewCenter: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
// leave 20% for the numbers area
let diameter = bounds.width * 0.8
// dart board radii in mm
let specRadii: [CGFloat] = [
170, 162, 107, 99, 16, 6
]
// convert to view size
let factor: CGFloat = (diameter * 0.5) / specRadii[0]
outerDoubleRadius = specRadii[0] * factor
innerDoubleRadius = specRadii[1] * factor
outerTripleRadius = specRadii[2] * factor
innerTripleRadius = specRadii[3] * factor
outerBullRadius = specRadii[4] * factor
innerBullRadius = specRadii[5] * factor
let wireColor: UIColor = UIColor(white: 0.8, alpha: 1.0)
let wedgeWidth: CGFloat = 360.0 / 20.0
let incAngle: CGFloat = wedgeWidth.radians
startAngle = -(incAngle * 0.5)
var path: UIBezierPath = UIBezierPath()
// outer board layer
path = UIBezierPath(ovalIn: bounds)
boardLayer.path = path.cgPath
for i in 0..<20 {
let endAngle = startAngle + incAngle
var shape = doubleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerDoubleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: innerDoubleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
doubleSegments[i].path = path
shape.fillColor = doubleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = outerSingleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: innerDoubleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: outerTripleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
outerSingleSegments[i].path = path
shape.fillColor = outerSingleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = tripleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerTripleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: innerTripleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
tripleSegments[i].path = path
shape.fillColor = tripleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = innerSingleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: innerTripleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: outerBullRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
innerSingleSegments[i].path = path
shape.fillColor = innerSingleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
startAngle = endAngle
}
let singleBullPath = UIBezierPath(ovalIn: CGRect(x: viewCenter.x - outerBullRadius, y: viewCenter.y - outerBullRadius, width: outerBullRadius * 2, height: outerBullRadius * 2))
let doubleBullPath = UIBezierPath(ovalIn: CGRect(x: viewCenter.x - innerBullRadius, y: viewCenter.y - innerBullRadius, width: innerBullRadius * 2, height: innerBullRadius * 2))
var shape = singleBullSegment.layer
singleBullPath.append(doubleBullPath)
singleBullPath.usesEvenOddFillRule = true
shape.fillRule = .evenOdd
shape.path = singleBullPath.cgPath
singleBullSegment.path = singleBullPath
shape.fillColor = singleBullSegment.color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = doubleBullSegment.layer
shape.path = doubleBullPath.cgPath
doubleBullSegment.path = doubleBullPath
shape.fillColor = doubleBullSegment.color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
// append all segments for hit-testing
allSegments = []
allSegments.append(contentsOf: tripleSegments)
allSegments.append(contentsOf: outerSingleSegments)
allSegments.append(contentsOf: doubleSegments)
allSegments.append(contentsOf: innerSingleSegments)
allSegments.append(singleBullSegment)
allSegments.append(doubleBullSegment)
}
}
CGFloat 扩展
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
示例视图控制器
class DartBoardViewController: UIViewController {
let dartBoard = DartBoardView()
override func viewDidLoad() {
super.viewDidLoad()
dartBoard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dartBoard)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
dartBoard.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dartBoard.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dartBoard.heightAnchor.constraint(equalTo: dartBoard.widthAnchor),
dartBoard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
dartBoard.backgroundColor = .clear
}
}
结果:
点击几下即可调试输出:
HIT: Double 20
HIT: Single 18
HIT: Triple 2
HIT: Single 25
HIT: Double 25