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

Swift 3: Animate color fill of arc added to UIBezierPath

我想为饼图的一部分填充颜色设置动画。我通过为每个饼图创建一个 UIBezierPath() 来创建饼图,然后使用 addArc 方法指定圆弧的 size/constraints。要为饼图段设置动画,我希望圆弧的颜色填充从圆心到半径端设置动画。但是,我遇到了麻烦。我听说从 01strokeEnd 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
    }
}

产生:

动画的延迟使其更具动态效果。

如果你想玩得花哨,你可以玩一下整个 CAShapeLayertransform 的动画。例如,您可以缩放饼图的楔形:

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
}

应用于每个饼图切片的每个动画都将是从 01strokeEnd 关键路径动画。创建掩码时,确保其 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)
    }
}