具有 .round lineCap 的多个 CAShapeLayers 相互重叠
Multiple CAShapeLayers with .round lineCap are overlapped by each other
我尝试使用 .round
lineCap
和半圆路径组合多层:
let layer = CAShapeLayer()
layer.lineWidth = 12
layer.lineCap = .round
layer.strokeColor = color.withAlphaComponent(0.32).cgColor
layer.fillColor = UIColor.clear.cgColor
// angles calculation
let path = UIBezierPath(arcCenter: arcCenter,
radius: radius,
startAngle: startAngle,
endAngle: engAngle,
clockwise: true)
layer.path = path.cgPath
努力实现以下目标:
但是我的图层相互重叠。是否有可能以某种方式快速修复它,或者我是否需要手动实现圆角路径计算?
当.lineCap = .round
我们得到一个以端点为中心、半径为线宽1/2的“圆”:
所以,为了让圆圆的端点位于端点,我们可以通过 asin(lineWidth * 0.5 / radius)
:
调整端点
假设我们按顺时针方向前进:
let delta: CGFloat = lineCap == .round ? asin(lineWidth * 0.5 / radius) : 0.0
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startDegrees.radians + delta,
endAngle: endDegrees.radians - delta,
clockwise: true)
所以,如果我们用 .lineEnd = .butt
(默认值)形成此图像的一系列度数:
我们可以通过偏移开始角和结束角来得到这个:
这是一个完整的例子class:
class ConnectedArcsView: UIView {
public var segmentDegrees: [CGFloat] = [] {
didSet {
var n: Int = 0
if let subs = layer.sublayers {
n = subs.count
if n > segmentDegrees.count {
// if we already have sublayers,
// remove any extras
for _ in 0..<n - segmentDegrees.count {
subs.last?.removeFromSuperlayer()
}
}
}
// add sublayers if needed
while n < segmentDegrees.count {
let l = CAShapeLayer()
l.fillColor = UIColor.clear.cgColor
layer.addSublayer(l)
n += 1
}
setNeedsLayout()
}
}
// segment colors default: [.red, .green, .blue]
public var segmentColors: [UIColor] = [.red, .green, .blue] { didSet { setNeedsLayout() } }
// line width default: 12
public var lineWidth: CGFloat = 12 { didSet { setNeedsLayout() } }
// line cap default: .round
public var lineCap: CAShapeLayerLineCap = .round { didSet { setNeedsLayout() } }
override func layoutSubviews() {
super.layoutSubviews()
guard let subs = layer.sublayers else { return }
let radius = (bounds.size.width - lineWidth) * 0.5
let center = CGPoint(x: bounds.midX, y: bounds.midY)
// if lineCap == .round
// calculate delta for start and end angles
let delta: CGFloat = lineCap == .round ? asin(lineWidth * 0.5 / radius) : 0.0
// calculate start angle so the "gap" is centered at the bottom
let totalDegrees: CGFloat = segmentDegrees.reduce(0.0, +)
var startDegrees: CGFloat = 90.0 + (360.0 - totalDegrees) * 0.5
for i in 0..<segmentDegrees.count {
let endDegrees = startDegrees + segmentDegrees[i]
guard let shape = subs[i] as? CAShapeLayer else { continue }
shape.lineWidth = lineWidth
shape.lineCap = lineCap
shape.strokeColor = segmentColors[i % segmentColors.count].cgColor
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startDegrees.radians + delta,
endAngle: endDegrees.radians - delta,
clockwise: true)
shape.path = path.cgPath
startDegrees += segmentDegrees[i]
}
}
}
以及显示其用法的示例视图控制器:
class ExampleViewController: UIViewController {
let testView = ConnectedArcsView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
let degrees: [CGFloat] = [
40, 25, 140, 25, 40
]
let colors: [UIColor] = [
.systemRed, .systemYellow, .systemGreen, .systemYellow, .systemRed
] //.map { ([=12=] as UIColor).withAlphaComponent(0.5) }
testView.segmentDegrees = degrees
testView.segmentColors = colors
testView.lineWidth = 12
testView.backgroundColor = .black
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
// add an info label
let v = UILabel()
v.textAlignment = .center
v.numberOfLines = 0
v.text = "Tap anywhere to toggle between\n\".round\" and \".butt\""
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
testView.heightAnchor.constraint(equalTo: testView.widthAnchor),
v.topAnchor.constraint(equalTo: testView.bottomAnchor, constant: 20.0),
v.widthAnchor.constraint(equalTo: testView.widthAnchor),
v.centerXAnchor.constraint(equalTo: testView.centerXAnchor),
])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
testView.lineCap = testView.lineCap == .round ? .butt : .round
}
}
当 运行 时,点击任意位置将在 .round
和 .butt
之间切换。
编辑 - 忘记包含辅助扩展:
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
我尝试使用 .round
lineCap
和半圆路径组合多层:
let layer = CAShapeLayer()
layer.lineWidth = 12
layer.lineCap = .round
layer.strokeColor = color.withAlphaComponent(0.32).cgColor
layer.fillColor = UIColor.clear.cgColor
// angles calculation
let path = UIBezierPath(arcCenter: arcCenter,
radius: radius,
startAngle: startAngle,
endAngle: engAngle,
clockwise: true)
layer.path = path.cgPath
努力实现以下目标:
但是我的图层相互重叠。是否有可能以某种方式快速修复它,或者我是否需要手动实现圆角路径计算?
当.lineCap = .round
我们得到一个以端点为中心、半径为线宽1/2的“圆”:
所以,为了让圆圆的端点位于端点,我们可以通过 asin(lineWidth * 0.5 / radius)
:
假设我们按顺时针方向前进:
let delta: CGFloat = lineCap == .round ? asin(lineWidth * 0.5 / radius) : 0.0
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startDegrees.radians + delta,
endAngle: endDegrees.radians - delta,
clockwise: true)
所以,如果我们用 .lineEnd = .butt
(默认值)形成此图像的一系列度数:
我们可以通过偏移开始角和结束角来得到这个:
这是一个完整的例子class:
class ConnectedArcsView: UIView {
public var segmentDegrees: [CGFloat] = [] {
didSet {
var n: Int = 0
if let subs = layer.sublayers {
n = subs.count
if n > segmentDegrees.count {
// if we already have sublayers,
// remove any extras
for _ in 0..<n - segmentDegrees.count {
subs.last?.removeFromSuperlayer()
}
}
}
// add sublayers if needed
while n < segmentDegrees.count {
let l = CAShapeLayer()
l.fillColor = UIColor.clear.cgColor
layer.addSublayer(l)
n += 1
}
setNeedsLayout()
}
}
// segment colors default: [.red, .green, .blue]
public var segmentColors: [UIColor] = [.red, .green, .blue] { didSet { setNeedsLayout() } }
// line width default: 12
public var lineWidth: CGFloat = 12 { didSet { setNeedsLayout() } }
// line cap default: .round
public var lineCap: CAShapeLayerLineCap = .round { didSet { setNeedsLayout() } }
override func layoutSubviews() {
super.layoutSubviews()
guard let subs = layer.sublayers else { return }
let radius = (bounds.size.width - lineWidth) * 0.5
let center = CGPoint(x: bounds.midX, y: bounds.midY)
// if lineCap == .round
// calculate delta for start and end angles
let delta: CGFloat = lineCap == .round ? asin(lineWidth * 0.5 / radius) : 0.0
// calculate start angle so the "gap" is centered at the bottom
let totalDegrees: CGFloat = segmentDegrees.reduce(0.0, +)
var startDegrees: CGFloat = 90.0 + (360.0 - totalDegrees) * 0.5
for i in 0..<segmentDegrees.count {
let endDegrees = startDegrees + segmentDegrees[i]
guard let shape = subs[i] as? CAShapeLayer else { continue }
shape.lineWidth = lineWidth
shape.lineCap = lineCap
shape.strokeColor = segmentColors[i % segmentColors.count].cgColor
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startDegrees.radians + delta,
endAngle: endDegrees.radians - delta,
clockwise: true)
shape.path = path.cgPath
startDegrees += segmentDegrees[i]
}
}
}
以及显示其用法的示例视图控制器:
class ExampleViewController: UIViewController {
let testView = ConnectedArcsView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
let degrees: [CGFloat] = [
40, 25, 140, 25, 40
]
let colors: [UIColor] = [
.systemRed, .systemYellow, .systemGreen, .systemYellow, .systemRed
] //.map { ([=12=] as UIColor).withAlphaComponent(0.5) }
testView.segmentDegrees = degrees
testView.segmentColors = colors
testView.lineWidth = 12
testView.backgroundColor = .black
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
// add an info label
let v = UILabel()
v.textAlignment = .center
v.numberOfLines = 0
v.text = "Tap anywhere to toggle between\n\".round\" and \".butt\""
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
testView.heightAnchor.constraint(equalTo: testView.widthAnchor),
v.topAnchor.constraint(equalTo: testView.bottomAnchor, constant: 20.0),
v.widthAnchor.constraint(equalTo: testView.widthAnchor),
v.centerXAnchor.constraint(equalTo: testView.centerXAnchor),
])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
testView.lineCap = testView.lineCap == .round ? .butt : .round
}
}
当 运行 时,点击任意位置将在 .round
和 .butt
之间切换。
编辑 - 忘记包含辅助扩展:
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}