[英]Swift 5 and UIKit draw, animate and split lines between 2 to 3 UIViews
经过一番研究,我为 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")
}
}
你在正确的轨道上...
绘制“分割”线的问题在于有一个起点和两个终点。 因此,生成的 animation 可能不是您真正想要的。
另一种方法是使用两个图层 - 一个带有“左侧”分割线,一个带有“右侧”分割线,然后将它们一起制作动画。
这是将事物包装到“连接”视图子类中的示例。
我们将使用 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)
}
}
}
和一个示例视图 controller 显示它的运行情况:
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
}
}
}
运行将给出以下结果:
底部的第一个按钮将切换 Top-Center 和 Top-Left-Right 之间的连接(每次重新运行 animation)。 第二个按钮将在透明和黄色之间切换视图的背景颜色,以便我们可以看到它的框架。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.