繁体   English   中英

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

[英]Swift 3: Animate color fill of arc added to UIBezierPath

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

我在这里看到了类似问题的解决方案,但它不适用于单个弧。

strokeEnd设置动画strokeEnd ,会为路径周围的笔触设置动画,而不是路径的填充。

如果您只是在寻找填充的任何动画,简单的选项包括设置UIColor.clear.cgColor到最终颜色的fillColor关键路径的动画。 或为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
    }
}

产生:

在此处输入图片说明

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

如果想花哨的话,可以CAShapeLayer整个CAShapeLayertransform动画。 例如,您可以缩放饼图楔形:

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
    }
}

屈服:

在此处输入图片说明

或者,您可以绕其中心角旋转饼形形状图层,使其看起来在角度上扩展:

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
    }
}

产生:

在此处输入图片说明

我鼓励您不要对我的CAShapeLayer和模型的细节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

这听起来像是您想要的“时钟擦除”效果可以显示您的图表。 如果是这种情况,那么有一种比为饼图的每个单独的楔形创建单独的遮罩层更简单的方法。 而是使图形的每个楔形成为单层的子层,在该超级层上安装遮罩层,然后在该超级层上运行时钟擦除动画。

这是说明静态图像的时钟擦除动画的GIF:

时钟擦除动画

我写了一篇文章解释如何做,并链接到一个演示它的Github项目:

如何在iOS中实现“时钟擦除” /径向擦除效果?

将圆弧radius减小一半,并使线的厚度是半径的两倍

let path = closedArc(at: center, with: radius * 0.5, start: startAngle, end: endAngle)
lineLayer.lineWidth = radius

fillColor设置为clear

在此处输入图像描述 我可能会迟到。 但如果有人正在寻找平滑的圆形饼图 animation 试试这个:-

在你的 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
    }
}

在 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