[英]Swift 3: Animate color fill of arc added to UIBezierPath
I wish to animate the color fill of a section of a pie chart.我希望为饼图的一部分的颜色填充设置动画。 I create the pie chart by creating a
UIBezierPath()
for each piece of the pie and then use the addArc
method to specify the size/constraints of the arc.我通过为每个饼图创建一个
UIBezierPath()
来创建饼图,然后使用addArc
方法指定圆弧的大小/约束。 To animate the pie chart segment, I want the color fill of the arc to animate from the center of the circle to the radius end.要为饼图段设置动画,我希望圆弧的颜色填充从圆心到半径端设置动画。 However, I am having trouble.
但是,我遇到了麻烦。 I heard the
strokeEnd
keyPath
animated from 0
to 1
should work, but there is no animation happening on the arcs (the arcs are just appearing at app launch).我听说从
0
到1
的strokeEnd
keyPath
动画应该可以工作,但是弧上没有 animation 发生(弧只是出现在应用程序启动时)。
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)
}
I've seen a solution to a similar problem here , but it does not work for the individual arcs.我在这里看到了类似问题的解决方案,但它不适用于单个弧。
When you animate strokeEnd
, that animates the stroke around the path, but not the fill of the path. 为
strokeEnd
设置动画strokeEnd
,会为路径周围的笔触设置动画,而不是路径的填充。
If you're looking for just any animation of the fill, easy options include animating the fillColor
key path from UIColor.clear.cgColor
to the final color. 如果您只是在寻找填充的任何动画,简单的选项包括设置
UIColor.clear.cgColor
到最终颜色的fillColor
关键路径的动画。 Or animate the opacity
key path from 0 to 1. 或为
opacity
键路径设置从0到1的动画。
func addPie(_ animated: Bool = true) {
shapeLayers.forEach { $0.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) { $0 + $1.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
}
}
That yields: 产生:
The delaying of the animations give it a slightly more dynamic effect. 动画的延迟使它具有更动态的效果。
If you want to get fancy, you can play around with animations of transform
of the entire CAShapeLayer
. 如果想花哨的话,可以
CAShapeLayer
整个CAShapeLayer
的transform
动画。 For example, you can scale the pie wedges: 例如,您可以缩放饼图楔形:
func addPie(_ animated: Bool = true) {
shapeLayers.forEach { $0.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) { $0 + $1.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
}
}
Yielding: 屈服:
Or you can rotate the pie wedge shape layer about its center angle making it appear to angularly expand: 或者,您可以绕其中心角旋转饼形形状图层,使其看起来在角度上扩展:
func addPie(_ animated: Bool = true) {
shapeLayers.forEach { $0.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) { $0 + $1.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
}
}
That yields: 产生:
I'd encourage you to not get too lost in the details of my CAShapeLayer
and my model, but rather focus on the CABasicAnimation
and the various keyPath
values we can animate. 我鼓励您不要对我的
CAShapeLayer
和模型的细节CAShapeLayer
,而应该专注于CABasicAnimation
和我们可以设置动画的各种keyPath
值。
In order to ensure a smooth, clockwise animation of the pie chart, you must perform the following steps in order: 为了确保饼图的顺时针顺畅动画,必须按顺序执行以下步骤:
CAShapeLayer
CAShapeLayer
的新父层 self.layer.addSublayer(parentLayer)
self.layer.addSublayer(parentLayer)
In a nutshell, the code will look like this: 简而言之,代码将如下所示:
// 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
}
Each animation applied to each pie slice will be a strokeEnd
keypath animation from 0
to 1
. 应用于每个饼图的每个动画都是从
0
到1
的strokeEnd
关键路径动画。 When creating the mask, be sure its fillColor
property is set to UIColor.clear.cgColor
. 创建遮罩时,请确保将其
fillColor
属性设置为UIColor.clear.cgColor
。
It sounds like what you are after is a "clock wipe" effect that reveals your graph. 这听起来像是您想要的“时钟擦除”效果可以显示您的图表。 If that's the case then there is a simpler way than creating a separate mask layer for each separate wedge of your pie chart.
如果是这种情况,那么有一种比为饼图的每个单独的楔形创建单独的遮罩层更简单的方法。 Instead, make each wedge of your graph a sublayer of a single layer, install a mask layer on that super-layer, and run a clock wipe animation on that super layer.
而是使图形的每个楔形成为单层的子层,在该超级层上安装遮罩层,然后在该超级层上运行时钟擦除动画。
Here is a GIF illustrating a clock wipe animation of a static image: 这是说明静态图像的时钟擦除动画的GIF:
I wrote a post explaining how to do it, and linking to a Github project demonstrating it: 我写了一篇文章解释如何做,并链接到一个演示它的Github项目:
How do you achieve a "clock wipe"/ radial wipe effect in iOS? 如何在iOS中实现“时钟擦除” /径向擦除效果?
reduce the arc radius
to half and make the line twice as thick as the radius 将圆弧
radius
减小一半,并使线的厚度是半径的两倍
let path = closedArc(at: center, with: radius * 0.5, start: startAngle, end: endAngle)
lineLayer.lineWidth = radius
set the fillColor
to clear
将
fillColor
设置为clear
I may be very late to the party.
我可能会迟到。 But if anyone are looking for smooth circle Pie chart animation try this:-
但如果有人正在寻找平滑的圆形饼图 animation 试试这个:-
In your ViewController class use below在你的 ViewController class 下面使用
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
}
}
Create below PieChartView class which will animate pieChart as well在 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, {$0 + $1.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)
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.