Swift 3: 添加到 UIBezierPath 的圆弧动画颜色填充
Swift 3: Animate color fill of arc added to UIBezierPath
我想为饼图的一部分填充颜色设置动画。我通过为每个饼图创建一个 UIBezierPath()
来创建饼图,然后使用 addArc
方法指定圆弧的 size/constraints。要为饼图段设置动画,我希望圆弧的颜色填充从圆心到半径端设置动画。但是,我遇到了麻烦。我听说从 0
到 1
的 strokeEnd
keyPath
动画应该可以工作,但是弧上没有动画发生(弧只是出现在应用程序启动时)。
let rad = 2 * Double.pi
let pieCenter: CGPoint = CGPoint(x: frame.width / 2, y: frame.height / 2)
var start: Double = 0
for i in 0...data.count - 1 {
let size: Double = Double(data[i])! / 100 // the percentege of the circle that the given arc will take
let end: Double = start + (size * rad)
let path = UIBezierPath()
path.move(to: pieCenter)
path.addArc(withCenter: pieCenter, radius: frame.width / 3, startAngle: CGFloat(start), endAngle: CGFloat(end), clockwise: true)
start += size * rad
let lineLayer = CAShapeLayer()
lineLayer.bounds = self.bounds
lineLayer.position = self.layer.position
lineLayer.path = path.cgPath
lineLayer.strokeColor = UIColor.white.cgColor
lineLayer.fillColor = colors[i]
lineLayer.lineWidth = 0
self.layer.addSublayer(lineLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fillMode = kCAFillModeForwards
animation.fromValue = pieCenter
animation.toValue = frame.width / 3 // radius
animation.duration = 2.5
lineLayer.add(animation, forKey: nil)
}
我见过类似问题的解决方案 here,但它不适用于单个弧。
当您为 strokeEnd
设置动画时,会为路径周围的笔画设置动画,但不会为路径填充设置动画。
如果您正在寻找任何填充动画,简单的选项包括为从 UIColor.clear.cgColor
到最终颜色的 fillColor
关键路径设置动画。或者将 opacity
键路径从 0 动画化到 1。
func addPie(_ animated: Bool = true) {
shapeLayers.forEach { [=10=].removeFromSuperlayer() }
shapeLayers.removeAll()
guard let dataPoints = dataPoints else { return }
let center = pieCenter
let radius = pieRadius
var startAngle = -CGFloat.pi / 2
let sum = dataPoints.reduce(0.0) { [=10=] + .value }
for (index, dataPoint) in dataPoints.enumerated() {
let endAngle = startAngle + CGFloat(dataPoint.value / sum) * 2 * .pi
let path = closedArc(at: center, with: radius, start: startAngle, end: endAngle)
let shape = CAShapeLayer()
shape.fillColor = dataPoint.color.cgColor
shape.strokeColor = UIColor.black.cgColor
shape.lineWidth = lineWidth
shape.path = path.cgPath
layer.addSublayer(shape)
shapeLayers.append(shape)
shape.frame = bounds
if animated {
shape.opacity = 0
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) / Double(dataPoints.count)) {
shape.opacity = 1
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 1
shape.add(animation, forKey: nil)
}
}
startAngle = endAngle
}
}
产生:
动画的延迟使其更具动态效果。
如果你想玩得花哨,你可以玩一下整个 CAShapeLayer
的 transform
的动画。例如,您可以缩放饼图的楔形:
func addPie(_ animated: Bool = true) {
shapeLayers.forEach { [=11=].removeFromSuperlayer() }
shapeLayers.removeAll()
guard let dataPoints = dataPoints else { return }
let center = pieCenter
let radius = pieRadius
var startAngle = -CGFloat.pi / 2
let sum = dataPoints.reduce(0.0) { [=11=] + .value }
for (index, dataPoint) in dataPoints.enumerated() {
let endAngle = startAngle + CGFloat(dataPoint.value / sum) * 2 * .pi
let path = closedArc(at: center, with: radius, start: startAngle, end: endAngle)
let shape = CAShapeLayer()
shape.fillColor = dataPoint.color.cgColor
shape.strokeColor = UIColor.black.cgColor
shape.lineWidth = lineWidth
shape.path = path.cgPath
layer.addSublayer(shape)
shapeLayers.append(shape)
shape.frame = bounds
if animated {
shape.opacity = 0
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) / Double(dataPoints.count) + 1) {
shape.opacity = 1
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = CATransform3DMakeScale(0, 0, 1)
animation.toValue = CATransform3DIdentity
animation.duration = 1
shape.add(animation, forKey: nil)
}
}
startAngle = endAngle
}
}
产量:
或者您可以围绕其中心角旋转饼楔形图层,使其看起来呈角度展开:
func addPie(_ animated: Bool = true) {
shapeLayers.forEach { [=12=].removeFromSuperlayer() }
shapeLayers.removeAll()
guard let dataPoints = dataPoints else { return }
let center = pieCenter
let radius = pieRadius
var startAngle = -CGFloat.pi / 2
let sum = dataPoints.reduce(0.0) { [=12=] + .value }
for (index, dataPoint) in dataPoints.enumerated() {
let endAngle = startAngle + CGFloat(dataPoint.value / sum) * 2 * .pi
let path = closedArc(at: center, with: radius, start: startAngle, end: endAngle)
let shape = CAShapeLayer()
shape.fillColor = dataPoint.color.cgColor
shape.strokeColor = UIColor.black.cgColor
shape.lineWidth = lineWidth
shape.path = path.cgPath
layer.addSublayer(shape)
shapeLayers.append(shape)
shape.frame = bounds
if animated {
shape.opacity = 0
let centerAngle = startAngle + CGFloat(dataPoint.value / sum) * .pi
let transform = CATransform3DMakeRotation(.pi / 2, cos(centerAngle), sin(centerAngle), 0)
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) / Double(dataPoints.count)) {
shape.opacity = 1
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = transform
animation.toValue = CATransform3DIdentity
animation.duration = 1
shape.add(animation, forKey: nil)
}
}
startAngle = endAngle
}
}
产生:
我鼓励您不要过于迷失在我的 CAShapeLayer
和模型的细节中,而是关注 CABasicAnimation
和我们可以得到的各种 keyPath
值动画。
为了保证饼图的顺时针动画流畅,必须按顺序执行以下步骤:
- 创建类型为
CAShapeLayer
的新父图层
- 循环每个饼图切片到父层
- 在另一个循环中,遍历父层的子层(饼图切片)并为每个子层分配一个遮罩并在循环中为该遮罩设置动画
- 将父层添加到主层:
self.layer.addSublayer(parentLayer)
简而言之,代码如下所示:
// Code above this line adds the pie chart slices to the parentLayer
for layer in parentLayer.sublayers! {
// Code in this block creates and animates the same mask for each layer
}
应用于每个饼图切片的每个动画都将是从 0
到 1
的 strokeEnd
关键路径动画。创建掩码时,确保其 fillColor
属性 设置为 UIColor.clear.cgColor
.
听起来您所追求的是显示图表的 "clock wipe" 效果。如果是这种情况,那么有一种比为饼图的每个单独楔形创建单独的遮罩层更简单的方法。取而代之的是,让图形的每个楔形成为单个层的子层,在该超层上安装遮罩层,并在该超层上 运行 时钟擦除动画。
这是一个 GIF,展示了静态图像的时钟擦除动画:
我写了一个 post 解释了如何做,并链接到一个 Github 项目来演示它:
How do you achieve a "clock wipe"/ radial wipe effect in iOS?
将圆弧radius
减半,使线的粗度是半径的两倍
let path = closedArc(at: center, with: radius * 0.5, start: startAngle, end: endAngle)
lineLayer.lineWidth = radius
将 fillColor
设置为 clear
我可能会迟到。但是如果有人正在寻找平滑的圆形饼图动画,试试这个:-
在你的ViewControllerclass下面使用
class YourViewController: UIViewController {
@IBOutlet weak var myView: UIView!
let chartView = PieChartView()
override func viewDidLoad() {
super.viewDidLoad()
myView.addSubview(chartView)
chartView.translatesAutoresizingMaskIntoConstraints = false
chartView.leftAnchor.constraint(equalTo: myView.leftAnchor).isActive = true
chartView.rightAnchor.constraint(equalTo: myView.rightAnchor).isActive = true
chartView.topAnchor.constraint(equalTo: myView.topAnchor).isActive = true
chartView.bottomAnchor.constraint(equalTo: myView.bottomAnchor).isActive = true
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let segments = [Segment(value: 70, color: .systemBlue), Segment(value: 40, color: .systemCyan), Segment(value: 5, color: .systemTeal), Segment(value: 4, color: .systemMint), Segment(value: 5, color: .systemPurple)]
chartView.segments = segments
}
}
在 PieChartView 下面创建 class,这也将动画化 pieChart
class PieChartView: UIView {
/// An array of structs representing the segments of the pie chart
var pieSliceLayer = CAShapeLayer()
var count = 0
var startAngle = -CGFloat.pi * 0.5
var segments = [Segment]() {
didSet {
setNeedsDisplay() // re-draw view when the values get set
}
}
override init(frame: CGRect) {
super.init(frame: frame)
isOpaque = false // when overriding drawRect, you must specify this to maintain transparency.
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
addSlices()
}
func addSlices() {
guard count < segments.count else {return}
let radius = min(bounds.width, bounds.height) / 2.0 - 20
let center: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let valueCount = segments.reduce(0, {[=11=] + .value})
for value in segments {
let pieSliceLayer = CAShapeLayer()
pieSliceLayer.strokeColor = value.color.cgColor
pieSliceLayer.fillColor = UIColor.clear.cgColor
pieSliceLayer.lineWidth = radius
layer.addSublayer(pieSliceLayer)
let endAngle = CGFloat(value.value) / valueCount * CGFloat.pi * 2.0 + startAngle
pieSliceLayer.path = UIBezierPath(arcCenter: center, radius: radius/2, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
startAngle = endAngle
}
pieSliceLayer.strokeColor = UIColor.white.cgColor
pieSliceLayer.fillColor = UIColor.clear.cgColor
pieSliceLayer.lineWidth = radius
let startAngle: CGFloat = 3 * .pi / 2
let endAngle: CGFloat = -3 * .pi / 2
pieSliceLayer.path = UIBezierPath(arcCenter: center, radius: radius/2, startAngle: startAngle, endAngle: endAngle, clockwise: false).cgPath
pieSliceLayer.strokeEnd = 1
layer.addSublayer(pieSliceLayer)
startCircleAnimation()
}
private func startCircleAnimation() {
pieSliceLayer.strokeEnd = 0
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 1
animation.toValue = 0
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.42, 0.00, 0.09, 1.00)
pieSliceLayer.add(animation, forKey: nil)
}
}
我想为饼图的一部分填充颜色设置动画。我通过为每个饼图创建一个 UIBezierPath()
来创建饼图,然后使用 addArc
方法指定圆弧的 size/constraints。要为饼图段设置动画,我希望圆弧的颜色填充从圆心到半径端设置动画。但是,我遇到了麻烦。我听说从 0
到 1
的 strokeEnd
keyPath
动画应该可以工作,但是弧上没有动画发生(弧只是出现在应用程序启动时)。
let rad = 2 * Double.pi
let pieCenter: CGPoint = CGPoint(x: frame.width / 2, y: frame.height / 2)
var start: Double = 0
for i in 0...data.count - 1 {
let size: Double = Double(data[i])! / 100 // the percentege of the circle that the given arc will take
let end: Double = start + (size * rad)
let path = UIBezierPath()
path.move(to: pieCenter)
path.addArc(withCenter: pieCenter, radius: frame.width / 3, startAngle: CGFloat(start), endAngle: CGFloat(end), clockwise: true)
start += size * rad
let lineLayer = CAShapeLayer()
lineLayer.bounds = self.bounds
lineLayer.position = self.layer.position
lineLayer.path = path.cgPath
lineLayer.strokeColor = UIColor.white.cgColor
lineLayer.fillColor = colors[i]
lineLayer.lineWidth = 0
self.layer.addSublayer(lineLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fillMode = kCAFillModeForwards
animation.fromValue = pieCenter
animation.toValue = frame.width / 3 // radius
animation.duration = 2.5
lineLayer.add(animation, forKey: nil)
}
我见过类似问题的解决方案 here,但它不适用于单个弧。
当您为 strokeEnd
设置动画时,会为路径周围的笔画设置动画,但不会为路径填充设置动画。
如果您正在寻找任何填充动画,简单的选项包括为从 UIColor.clear.cgColor
到最终颜色的 fillColor
关键路径设置动画。或者将 opacity
键路径从 0 动画化到 1。
func addPie(_ animated: Bool = true) {
shapeLayers.forEach { [=10=].removeFromSuperlayer() }
shapeLayers.removeAll()
guard let dataPoints = dataPoints else { return }
let center = pieCenter
let radius = pieRadius
var startAngle = -CGFloat.pi / 2
let sum = dataPoints.reduce(0.0) { [=10=] + .value }
for (index, dataPoint) in dataPoints.enumerated() {
let endAngle = startAngle + CGFloat(dataPoint.value / sum) * 2 * .pi
let path = closedArc(at: center, with: radius, start: startAngle, end: endAngle)
let shape = CAShapeLayer()
shape.fillColor = dataPoint.color.cgColor
shape.strokeColor = UIColor.black.cgColor
shape.lineWidth = lineWidth
shape.path = path.cgPath
layer.addSublayer(shape)
shapeLayers.append(shape)
shape.frame = bounds
if animated {
shape.opacity = 0
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) / Double(dataPoints.count)) {
shape.opacity = 1
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 1
shape.add(animation, forKey: nil)
}
}
startAngle = endAngle
}
}
产生:
动画的延迟使其更具动态效果。
如果你想玩得花哨,你可以玩一下整个 CAShapeLayer
的 transform
的动画。例如,您可以缩放饼图的楔形:
func addPie(_ animated: Bool = true) {
shapeLayers.forEach { [=11=].removeFromSuperlayer() }
shapeLayers.removeAll()
guard let dataPoints = dataPoints else { return }
let center = pieCenter
let radius = pieRadius
var startAngle = -CGFloat.pi / 2
let sum = dataPoints.reduce(0.0) { [=11=] + .value }
for (index, dataPoint) in dataPoints.enumerated() {
let endAngle = startAngle + CGFloat(dataPoint.value / sum) * 2 * .pi
let path = closedArc(at: center, with: radius, start: startAngle, end: endAngle)
let shape = CAShapeLayer()
shape.fillColor = dataPoint.color.cgColor
shape.strokeColor = UIColor.black.cgColor
shape.lineWidth = lineWidth
shape.path = path.cgPath
layer.addSublayer(shape)
shapeLayers.append(shape)
shape.frame = bounds
if animated {
shape.opacity = 0
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) / Double(dataPoints.count) + 1) {
shape.opacity = 1
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = CATransform3DMakeScale(0, 0, 1)
animation.toValue = CATransform3DIdentity
animation.duration = 1
shape.add(animation, forKey: nil)
}
}
startAngle = endAngle
}
}
产量:
或者您可以围绕其中心角旋转饼楔形图层,使其看起来呈角度展开:
func addPie(_ animated: Bool = true) {
shapeLayers.forEach { [=12=].removeFromSuperlayer() }
shapeLayers.removeAll()
guard let dataPoints = dataPoints else { return }
let center = pieCenter
let radius = pieRadius
var startAngle = -CGFloat.pi / 2
let sum = dataPoints.reduce(0.0) { [=12=] + .value }
for (index, dataPoint) in dataPoints.enumerated() {
let endAngle = startAngle + CGFloat(dataPoint.value / sum) * 2 * .pi
let path = closedArc(at: center, with: radius, start: startAngle, end: endAngle)
let shape = CAShapeLayer()
shape.fillColor = dataPoint.color.cgColor
shape.strokeColor = UIColor.black.cgColor
shape.lineWidth = lineWidth
shape.path = path.cgPath
layer.addSublayer(shape)
shapeLayers.append(shape)
shape.frame = bounds
if animated {
shape.opacity = 0
let centerAngle = startAngle + CGFloat(dataPoint.value / sum) * .pi
let transform = CATransform3DMakeRotation(.pi / 2, cos(centerAngle), sin(centerAngle), 0)
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) / Double(dataPoints.count)) {
shape.opacity = 1
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = transform
animation.toValue = CATransform3DIdentity
animation.duration = 1
shape.add(animation, forKey: nil)
}
}
startAngle = endAngle
}
}
产生:
我鼓励您不要过于迷失在我的 CAShapeLayer
和模型的细节中,而是关注 CABasicAnimation
和我们可以得到的各种 keyPath
值动画。
为了保证饼图的顺时针动画流畅,必须按顺序执行以下步骤:
- 创建类型为
CAShapeLayer
的新父图层
- 循环每个饼图切片到父层
- 在另一个循环中,遍历父层的子层(饼图切片)并为每个子层分配一个遮罩并在循环中为该遮罩设置动画
- 将父层添加到主层:
self.layer.addSublayer(parentLayer)
简而言之,代码如下所示:
// Code above this line adds the pie chart slices to the parentLayer
for layer in parentLayer.sublayers! {
// Code in this block creates and animates the same mask for each layer
}
应用于每个饼图切片的每个动画都将是从 0
到 1
的 strokeEnd
关键路径动画。创建掩码时,确保其 fillColor
属性 设置为 UIColor.clear.cgColor
.
听起来您所追求的是显示图表的 "clock wipe" 效果。如果是这种情况,那么有一种比为饼图的每个单独楔形创建单独的遮罩层更简单的方法。取而代之的是,让图形的每个楔形成为单个层的子层,在该超层上安装遮罩层,并在该超层上 运行 时钟擦除动画。
这是一个 GIF,展示了静态图像的时钟擦除动画:
我写了一个 post 解释了如何做,并链接到一个 Github 项目来演示它:
How do you achieve a "clock wipe"/ radial wipe effect in iOS?
将圆弧radius
减半,使线的粗度是半径的两倍
let path = closedArc(at: center, with: radius * 0.5, start: startAngle, end: endAngle)
lineLayer.lineWidth = radius
将 fillColor
设置为 clear
在你的ViewControllerclass下面使用
class YourViewController: UIViewController {
@IBOutlet weak var myView: UIView!
let chartView = PieChartView()
override func viewDidLoad() {
super.viewDidLoad()
myView.addSubview(chartView)
chartView.translatesAutoresizingMaskIntoConstraints = false
chartView.leftAnchor.constraint(equalTo: myView.leftAnchor).isActive = true
chartView.rightAnchor.constraint(equalTo: myView.rightAnchor).isActive = true
chartView.topAnchor.constraint(equalTo: myView.topAnchor).isActive = true
chartView.bottomAnchor.constraint(equalTo: myView.bottomAnchor).isActive = true
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let segments = [Segment(value: 70, color: .systemBlue), Segment(value: 40, color: .systemCyan), Segment(value: 5, color: .systemTeal), Segment(value: 4, color: .systemMint), Segment(value: 5, color: .systemPurple)]
chartView.segments = segments
}
}
在 PieChartView 下面创建 class,这也将动画化 pieChart
class PieChartView: UIView {
/// An array of structs representing the segments of the pie chart
var pieSliceLayer = CAShapeLayer()
var count = 0
var startAngle = -CGFloat.pi * 0.5
var segments = [Segment]() {
didSet {
setNeedsDisplay() // re-draw view when the values get set
}
}
override init(frame: CGRect) {
super.init(frame: frame)
isOpaque = false // when overriding drawRect, you must specify this to maintain transparency.
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
addSlices()
}
func addSlices() {
guard count < segments.count else {return}
let radius = min(bounds.width, bounds.height) / 2.0 - 20
let center: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let valueCount = segments.reduce(0, {[=11=] + .value})
for value in segments {
let pieSliceLayer = CAShapeLayer()
pieSliceLayer.strokeColor = value.color.cgColor
pieSliceLayer.fillColor = UIColor.clear.cgColor
pieSliceLayer.lineWidth = radius
layer.addSublayer(pieSliceLayer)
let endAngle = CGFloat(value.value) / valueCount * CGFloat.pi * 2.0 + startAngle
pieSliceLayer.path = UIBezierPath(arcCenter: center, radius: radius/2, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
startAngle = endAngle
}
pieSliceLayer.strokeColor = UIColor.white.cgColor
pieSliceLayer.fillColor = UIColor.clear.cgColor
pieSliceLayer.lineWidth = radius
let startAngle: CGFloat = 3 * .pi / 2
let endAngle: CGFloat = -3 * .pi / 2
pieSliceLayer.path = UIBezierPath(arcCenter: center, radius: radius/2, startAngle: startAngle, endAngle: endAngle, clockwise: false).cgPath
pieSliceLayer.strokeEnd = 1
layer.addSublayer(pieSliceLayer)
startCircleAnimation()
}
private func startCircleAnimation() {
pieSliceLayer.strokeEnd = 0
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 1
animation.toValue = 0
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.42, 0.00, 0.09, 1.00)
pieSliceLayer.add(animation, forKey: nil)
}
}