Swift 5 和 UIKit 在 2 到 3 个 UIView 之间绘制、动画和分割线
Swift 5 and UIKit draw, animate and split lines between 2 to 3 UIViews
我有一个可能包含 2 或 3 个 UIView 的视图。
我想绘制(并可能动画化一条从较高视图的底部 MidX 到底部的线。
如果我有 3 个视图,我希望线条拆分并为它们设置动画。
如果我只有一个视图,我希望使用 UIBezierPath 和 CAShapeLayer 将线条直接转到底部视图的中间顶部
考虑到屏幕高度(4.7" -> 6.2"),我附上了图片来说明我想要实现的目标。
感谢您的帮助。
经过一些研究,我想出了这个解决方案 Swift 5:
class ViewController: UIViewController {
@IBOutlet weak var someView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let start = CGPoint(x: self.someView.bounds.midX, y: self.someView.bounds.maxY)
let end = CGPoint(x: self.someView.layer.bounds.midX, y: (UIScreen.main.bounds.height / 2) - 100)
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
linePath.addLine(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.move(to: end)
linePath.addLine(to: CGPoint(x: 250, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.addLine(to: CGPoint(x: lowerViewA.x, y: lowerViewA.y))
linePath.move(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.addLine(to: CGPoint(x: lowerViewB.x, y: lowerViewB.y))
let shapeLayer = CAShapeLayer()
shapeLayer.path = linePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 2
shapeLayer.lineJoin = CAShapeLayerLineJoin.bevel
self.someView.layer.addSublayer(shapeLayer)
//Basic animation if you want to animate the line drawing.
let pathAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 4.0
pathAnimation.fromValue = 0.0
pathAnimation.toValue = 1.0
//Animation will happen right away
shapeLayer.add(pathAnimation, forKey: "strokeEnd")
}
}
你走在正确的轨道上...
绘制“分割”线的问题是有一个起点和 两个 终点。所以,生成的动画可能不是你真正想要的。
另一种方法是使用两层 - 一层带有“左侧”分割线,一层带有“右侧”分割线,然后将它们一起制作动画。
这是一个将事物包装到“连接”视图子类中的示例。
我们将使用 3 层:1 层用于单条垂直连接线,1 层用于右侧线和左侧线。
我们还可以将路径点设置为视图的中心,以及左右边缘。这样我们就可以将前缘限制在左框的中心,将后缘限制在右框的中心。
这个视图本身看起来像这样(背景为黄色,因此我们可以看到它的框架):
或:
线条将从顶部开始动画。
class ConnectView: UIView {
// determines whether we want a single box-to-box line, or
// left and right split / stepped lines to two boxes
public var single: Bool = true
private let singleLineLayer = CAShapeLayer()
private let leftLineLayer = CAShapeLayer()
private let rightLineLayer = CAShapeLayer()
private var durationFactor: CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add and configure sublayers
[singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
layer.addSublayer(lay)
lay.lineWidth = 4
lay.strokeColor = UIColor.blue.cgColor
lay.fillColor = UIColor.clear.cgColor
}
}
override func layoutSubviews() {
super.layoutSubviews()
// for readablility, define the points for our lines
let topCenter = CGPoint(x: bounds.midX, y: 0)
let midCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let botCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let midLeft = CGPoint(x: bounds.minX, y: bounds.midY)
let midRight = CGPoint(x: bounds.maxX, y: bounds.midY)
let botLeft = CGPoint(x: bounds.minX, y: bounds.maxY)
let botRight = CGPoint(x: bounds.maxX, y: bounds.maxY)
let singleBez = UIBezierPath()
let leftBez = UIBezierPath()
let rightBez = UIBezierPath()
// vertical line
singleBez.move(to: topCenter)
singleBez.addLine(to: botCenter)
// split / stepped line to the left
leftBez.move(to: topCenter)
leftBez.addLine(to: midCenter)
leftBez.addLine(to: midLeft)
leftBez.addLine(to: botLeft)
// split / stepped line to the right
rightBez.move(to: topCenter)
rightBez.addLine(to: midCenter)
rightBez.addLine(to: midRight)
rightBez.addLine(to: botRight)
// set the layer paths
// initializing strokeEnd to 0 for all three
singleLineLayer.path = singleBez.cgPath
singleLineLayer.strokeEnd = 0
leftLineLayer.path = leftBez.cgPath
leftLineLayer.strokeEnd = 0
rightLineLayer.path = rightBez.cgPath
rightLineLayer.strokeEnd = 0
// calculate total line lengths (in points)
// so we can adjust the "draw speed" in the animation
let singleLength = botCenter.y - topCenter.y
let doubleLength = singleLength + (midCenter.x - midLeft.x)
durationFactor = singleLength / doubleLength
}
public func doAnim() -> Void {
// reset the animations
[singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
lay.removeAllAnimations()
lay.strokeEnd = 0
}
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
if self.single {
// we want the apparent drawing speed to be the same
// for a single line as for a split / stepped line
// so change the animation duration
animation.duration *= durationFactor
// animate the single line layer
self.singleLineLayer.add(animation, forKey: animation.keyPath)
} else {
// animate the both left and right line layers
self.leftLineLayer.add(animation, forKey: animation.keyPath)
self.rightLineLayer.add(animation, forKey: animation.keyPath)
}
}
}
和一个展示它的示例视图控制器:
class ConnectTestViewController: UIViewController {
let vTop = UIView()
let vLeft = UIView()
let vCenter = UIView()
let vRight = UIView()
let testConnectView = ConnectView()
override func viewDidLoad() {
super.viewDidLoad()
// give the 4 views different background colors
// add them as subviews
// make them all 100x100 points
let colors: [UIColor] = [
.systemYellow,
.systemRed, .systemGreen, .systemBlue,
]
for (v, c) in zip([vTop, vLeft, vCenter, vRight], colors) {
v.backgroundColor = c
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
v.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
}
// add the clear-background Connect View
testConnectView.backgroundColor = .clear
testConnectView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testConnectView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// horizontally center the top box near the top
vTop.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
vTop.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// horizontally center the center box, 200-pts below the top box
vCenter.topAnchor.constraint(equalTo: vTop.bottomAnchor, constant: 200.0),
vCenter.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// align tops of left and right boxes with center box
vLeft.topAnchor.constraint(equalTo: vCenter.topAnchor),
vRight.topAnchor.constraint(equalTo: vCenter.topAnchor),
// position left and right boxes to left and right of center box
vLeft.trailingAnchor.constraint(equalTo: vCenter.leadingAnchor, constant: -20.0),
vRight.leadingAnchor.constraint(equalTo: vCenter.trailingAnchor, constant: 20.0),
// constrain Connect View
// Top to Bottom of Top box
testConnectView.topAnchor.constraint(equalTo: vTop.bottomAnchor),
// Bottom to Top of the row of 3 boxes
testConnectView.bottomAnchor.constraint(equalTo: vCenter.topAnchor),
// Leading to CenterX of Left box
testConnectView.leadingAnchor.constraint(equalTo: vLeft.centerXAnchor),
// Trailing to CenterX of Right box
testConnectView.trailingAnchor.constraint(equalTo: vRight.centerXAnchor),
])
// add a couple buttons at the bottom
let stack = UIStackView()
stack.spacing = 20
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
["Run Anim", "Show/Hide"].forEach { str in
let b = UIButton()
b.setTitle(str, for: [])
b.backgroundColor = .red
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.addTarget(self, action: #selector(buttonTap(_:)), for: .touchUpInside)
stack.addArrangedSubview(b)
}
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
stack.heightAnchor.constraint(equalToConstant: 50.0),
])
}
@objc func buttonTap(_ sender: Any?) -> Void {
guard let b = sender as? UIButton,
let t = b.currentTitle
else {
return
}
if t == "Run Anim" {
// tap button to toggle between
// Top-to-Middle box line or
// Top-to-SideBoxes split / stepped line
testConnectView.single.toggle()
// run the animation
testConnectView.doAnim()
} else {
// toggle background of Connect View between
// clear and yellow
testConnectView.backgroundColor = testConnectView.backgroundColor == .clear ? .yellow : .clear
}
}
}
运行 将给出此结果:
底部的第一个按钮将切换顶部中心和顶部左-右之间的连接(每次重新运行 动画)。第二个按钮将在透明和黄色之间切换视图的背景颜色,以便我们可以看到它的框架。
编辑
如果你想要像这样的圆角“台阶”:
将上面的 layoutSubviews()
代码替换为:
override func layoutSubviews() {
super.layoutSubviews()
// for readablility, define the points for our lines
let topCenter = CGPoint(x: bounds.midX, y: 0)
let midCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let botCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let midLeft = CGPoint(x: bounds.minX, y: bounds.midY)
let midRight = CGPoint(x: bounds.maxX, y: bounds.midY)
let botLeft = CGPoint(x: bounds.minX, y: bounds.maxY)
let botRight = CGPoint(x: bounds.maxX, y: bounds.maxY)
let singleBez = UIBezierPath()
let leftBez = UIBezierPath()
let rightBez = UIBezierPath()
// vertical line
singleBez.move(to: topCenter)
singleBez.addLine(to: botCenter)
// rounded "step" corners
let radius: CGFloat = 20.0
let leftArcP = CGPoint(x: midLeft.x + radius, y: midLeft.y)
let leftArcC = CGPoint(x: midLeft.x + radius, y: midLeft.y + radius)
let rightArcP = CGPoint(x: midRight.x - radius, y: midRight.y)
let rightArcC = CGPoint(x: midRight.x - radius, y: midRight.y + radius)
// split / stepped line to the left
leftBez.move(to: topCenter)
leftBez.addLine(to: midCenter)
leftBez.addLine(to: leftArcP)
leftBez.addArc(withCenter: leftArcC, radius: radius, startAngle: .pi * 1.5, endAngle: .pi, clockwise: false)
leftBez.addLine(to: botLeft)
// split / stepped line to the right
rightBez.move(to: topCenter)
rightBez.addLine(to: midCenter)
rightBez.addLine(to: rightArcP)
rightBez.addArc(withCenter: rightArcC, radius: radius, startAngle: .pi * 1.5, endAngle: 0, clockwise: true)
rightBez.addLine(to: botRight)
// set the layer paths
// initializing strokeEnd to 0 for all three
singleLineLayer.path = singleBez.cgPath
singleLineLayer.strokeEnd = 0
leftLineLayer.path = leftBez.cgPath
leftLineLayer.strokeEnd = 0
rightLineLayer.path = rightBez.cgPath
rightLineLayer.strokeEnd = 0
// calculate total line lengths (in points)
// so we can adjust the "draw speed" in the animation
let singleLength = botCenter.y - topCenter.y
let doubleLength = singleLength + (midCenter.x - midLeft.x)
durationFactor = singleLength / doubleLength
}
我有一个可能包含 2 或 3 个 UIView 的视图。
我想绘制(并可能动画化一条从较高视图的底部 MidX 到底部的线。
如果我有 3 个视图,我希望线条拆分并为它们设置动画。
如果我只有一个视图,我希望使用 UIBezierPath 和 CAShapeLayer 将线条直接转到底部视图的中间顶部
考虑到屏幕高度(4.7" -> 6.2"),我附上了图片来说明我想要实现的目标。
感谢您的帮助。
经过一些研究,我想出了这个解决方案 Swift 5:
class ViewController: UIViewController {
@IBOutlet weak var someView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let start = CGPoint(x: self.someView.bounds.midX, y: self.someView.bounds.maxY)
let end = CGPoint(x: self.someView.layer.bounds.midX, y: (UIScreen.main.bounds.height / 2) - 100)
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
linePath.addLine(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.move(to: end)
linePath.addLine(to: CGPoint(x: 250, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.addLine(to: CGPoint(x: lowerViewA.x, y: lowerViewA.y))
linePath.move(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.addLine(to: CGPoint(x: lowerViewB.x, y: lowerViewB.y))
let shapeLayer = CAShapeLayer()
shapeLayer.path = linePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 2
shapeLayer.lineJoin = CAShapeLayerLineJoin.bevel
self.someView.layer.addSublayer(shapeLayer)
//Basic animation if you want to animate the line drawing.
let pathAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 4.0
pathAnimation.fromValue = 0.0
pathAnimation.toValue = 1.0
//Animation will happen right away
shapeLayer.add(pathAnimation, forKey: "strokeEnd")
}
}
你走在正确的轨道上...
绘制“分割”线的问题是有一个起点和 两个 终点。所以,生成的动画可能不是你真正想要的。
另一种方法是使用两层 - 一层带有“左侧”分割线,一层带有“右侧”分割线,然后将它们一起制作动画。
这是一个将事物包装到“连接”视图子类中的示例。
我们将使用 3 层:1 层用于单条垂直连接线,1 层用于右侧线和左侧线。
我们还可以将路径点设置为视图的中心,以及左右边缘。这样我们就可以将前缘限制在左框的中心,将后缘限制在右框的中心。
这个视图本身看起来像这样(背景为黄色,因此我们可以看到它的框架):
或:
线条将从顶部开始动画。
class ConnectView: UIView {
// determines whether we want a single box-to-box line, or
// left and right split / stepped lines to two boxes
public var single: Bool = true
private let singleLineLayer = CAShapeLayer()
private let leftLineLayer = CAShapeLayer()
private let rightLineLayer = CAShapeLayer()
private var durationFactor: CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add and configure sublayers
[singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
layer.addSublayer(lay)
lay.lineWidth = 4
lay.strokeColor = UIColor.blue.cgColor
lay.fillColor = UIColor.clear.cgColor
}
}
override func layoutSubviews() {
super.layoutSubviews()
// for readablility, define the points for our lines
let topCenter = CGPoint(x: bounds.midX, y: 0)
let midCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let botCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let midLeft = CGPoint(x: bounds.minX, y: bounds.midY)
let midRight = CGPoint(x: bounds.maxX, y: bounds.midY)
let botLeft = CGPoint(x: bounds.minX, y: bounds.maxY)
let botRight = CGPoint(x: bounds.maxX, y: bounds.maxY)
let singleBez = UIBezierPath()
let leftBez = UIBezierPath()
let rightBez = UIBezierPath()
// vertical line
singleBez.move(to: topCenter)
singleBez.addLine(to: botCenter)
// split / stepped line to the left
leftBez.move(to: topCenter)
leftBez.addLine(to: midCenter)
leftBez.addLine(to: midLeft)
leftBez.addLine(to: botLeft)
// split / stepped line to the right
rightBez.move(to: topCenter)
rightBez.addLine(to: midCenter)
rightBez.addLine(to: midRight)
rightBez.addLine(to: botRight)
// set the layer paths
// initializing strokeEnd to 0 for all three
singleLineLayer.path = singleBez.cgPath
singleLineLayer.strokeEnd = 0
leftLineLayer.path = leftBez.cgPath
leftLineLayer.strokeEnd = 0
rightLineLayer.path = rightBez.cgPath
rightLineLayer.strokeEnd = 0
// calculate total line lengths (in points)
// so we can adjust the "draw speed" in the animation
let singleLength = botCenter.y - topCenter.y
let doubleLength = singleLength + (midCenter.x - midLeft.x)
durationFactor = singleLength / doubleLength
}
public func doAnim() -> Void {
// reset the animations
[singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
lay.removeAllAnimations()
lay.strokeEnd = 0
}
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
if self.single {
// we want the apparent drawing speed to be the same
// for a single line as for a split / stepped line
// so change the animation duration
animation.duration *= durationFactor
// animate the single line layer
self.singleLineLayer.add(animation, forKey: animation.keyPath)
} else {
// animate the both left and right line layers
self.leftLineLayer.add(animation, forKey: animation.keyPath)
self.rightLineLayer.add(animation, forKey: animation.keyPath)
}
}
}
和一个展示它的示例视图控制器:
class ConnectTestViewController: UIViewController {
let vTop = UIView()
let vLeft = UIView()
let vCenter = UIView()
let vRight = UIView()
let testConnectView = ConnectView()
override func viewDidLoad() {
super.viewDidLoad()
// give the 4 views different background colors
// add them as subviews
// make them all 100x100 points
let colors: [UIColor] = [
.systemYellow,
.systemRed, .systemGreen, .systemBlue,
]
for (v, c) in zip([vTop, vLeft, vCenter, vRight], colors) {
v.backgroundColor = c
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
v.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
}
// add the clear-background Connect View
testConnectView.backgroundColor = .clear
testConnectView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testConnectView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// horizontally center the top box near the top
vTop.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
vTop.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// horizontally center the center box, 200-pts below the top box
vCenter.topAnchor.constraint(equalTo: vTop.bottomAnchor, constant: 200.0),
vCenter.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// align tops of left and right boxes with center box
vLeft.topAnchor.constraint(equalTo: vCenter.topAnchor),
vRight.topAnchor.constraint(equalTo: vCenter.topAnchor),
// position left and right boxes to left and right of center box
vLeft.trailingAnchor.constraint(equalTo: vCenter.leadingAnchor, constant: -20.0),
vRight.leadingAnchor.constraint(equalTo: vCenter.trailingAnchor, constant: 20.0),
// constrain Connect View
// Top to Bottom of Top box
testConnectView.topAnchor.constraint(equalTo: vTop.bottomAnchor),
// Bottom to Top of the row of 3 boxes
testConnectView.bottomAnchor.constraint(equalTo: vCenter.topAnchor),
// Leading to CenterX of Left box
testConnectView.leadingAnchor.constraint(equalTo: vLeft.centerXAnchor),
// Trailing to CenterX of Right box
testConnectView.trailingAnchor.constraint(equalTo: vRight.centerXAnchor),
])
// add a couple buttons at the bottom
let stack = UIStackView()
stack.spacing = 20
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
["Run Anim", "Show/Hide"].forEach { str in
let b = UIButton()
b.setTitle(str, for: [])
b.backgroundColor = .red
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.addTarget(self, action: #selector(buttonTap(_:)), for: .touchUpInside)
stack.addArrangedSubview(b)
}
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
stack.heightAnchor.constraint(equalToConstant: 50.0),
])
}
@objc func buttonTap(_ sender: Any?) -> Void {
guard let b = sender as? UIButton,
let t = b.currentTitle
else {
return
}
if t == "Run Anim" {
// tap button to toggle between
// Top-to-Middle box line or
// Top-to-SideBoxes split / stepped line
testConnectView.single.toggle()
// run the animation
testConnectView.doAnim()
} else {
// toggle background of Connect View between
// clear and yellow
testConnectView.backgroundColor = testConnectView.backgroundColor == .clear ? .yellow : .clear
}
}
}
运行 将给出此结果:
底部的第一个按钮将切换顶部中心和顶部左-右之间的连接(每次重新运行 动画)。第二个按钮将在透明和黄色之间切换视图的背景颜色,以便我们可以看到它的框架。
编辑
如果你想要像这样的圆角“台阶”:
将上面的 layoutSubviews()
代码替换为:
override func layoutSubviews() {
super.layoutSubviews()
// for readablility, define the points for our lines
let topCenter = CGPoint(x: bounds.midX, y: 0)
let midCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let botCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let midLeft = CGPoint(x: bounds.minX, y: bounds.midY)
let midRight = CGPoint(x: bounds.maxX, y: bounds.midY)
let botLeft = CGPoint(x: bounds.minX, y: bounds.maxY)
let botRight = CGPoint(x: bounds.maxX, y: bounds.maxY)
let singleBez = UIBezierPath()
let leftBez = UIBezierPath()
let rightBez = UIBezierPath()
// vertical line
singleBez.move(to: topCenter)
singleBez.addLine(to: botCenter)
// rounded "step" corners
let radius: CGFloat = 20.0
let leftArcP = CGPoint(x: midLeft.x + radius, y: midLeft.y)
let leftArcC = CGPoint(x: midLeft.x + radius, y: midLeft.y + radius)
let rightArcP = CGPoint(x: midRight.x - radius, y: midRight.y)
let rightArcC = CGPoint(x: midRight.x - radius, y: midRight.y + radius)
// split / stepped line to the left
leftBez.move(to: topCenter)
leftBez.addLine(to: midCenter)
leftBez.addLine(to: leftArcP)
leftBez.addArc(withCenter: leftArcC, radius: radius, startAngle: .pi * 1.5, endAngle: .pi, clockwise: false)
leftBez.addLine(to: botLeft)
// split / stepped line to the right
rightBez.move(to: topCenter)
rightBez.addLine(to: midCenter)
rightBez.addLine(to: rightArcP)
rightBez.addArc(withCenter: rightArcC, radius: radius, startAngle: .pi * 1.5, endAngle: 0, clockwise: true)
rightBez.addLine(to: botRight)
// set the layer paths
// initializing strokeEnd to 0 for all three
singleLineLayer.path = singleBez.cgPath
singleLineLayer.strokeEnd = 0
leftLineLayer.path = leftBez.cgPath
leftLineLayer.strokeEnd = 0
rightLineLayer.path = rightBez.cgPath
rightLineLayer.strokeEnd = 0
// calculate total line lengths (in points)
// so we can adjust the "draw speed" in the animation
let singleLength = botCenter.y - topCenter.y
let doubleLength = singleLength + (midCenter.x - midLeft.x)
durationFactor = singleLength / doubleLength
}