简体   繁体   English

Swift 3:添加到 UIBezierPath 的圆弧动画颜色填充

[英]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).我听说从01strokeEnd 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整个CAShapeLayertransform动画。 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: 为了确保饼图的顺时针顺畅动画,必须按顺序执行以下步骤:

  • Create a new parent layer of type CAShapeLayer 创建一个类型为CAShapeLayer的新父层
  • In a loop each pie chart slice to the parent layer 在一个循环中,每个饼图切片都到达父层
  • In another loop, iterate through the sublayers (pie slices) of the parent layer and assign each sublayer a mask and animate that mask in the loop 在另一个循环中,循环遍历父层的子层(派生切片),并为每个子层分配一个遮罩,并在该循环中为该遮罩设置动画
  • Add the parent layer to the main layer: 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 . 应用于每个饼图的每个动画都是从01strokeEnd关键路径动画。 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM