Micrified
Micrified

Reputation: 3650

Properly animating circle radius with CABasicAnimation

I want to expand and contract a circle using CABasicAnimation. I previously designed a clean and effective way to draw the animation using sin and the drawRect method of the UIView class, but unfortunately it seems you can't animate or force drawRect to fire in UIView.animate methods no matter what you do. Therefore, I've had to come up with a workaround using CABasicAnimation. The result kind of works, but the animation is ugly and doesn't look right. Here's an illustration of what I mean:

enter image description here

Warping occurs during the expansion and contraction, and there appears to be a dent or cavity on the right side when the circle is expanding. Here's the class responsible for animating it:

struct PathConfig {
    var arcCenter: CGPoint
    var radius: CGFloat
    let startAngle: CGFloat = 0.0
    let endAngle: CGFloat = CGFloat(2 * M_PI)
}

class RGSStandbyIndicatorView: UIView {

    // MARK: - Variables & Constants

    /// The drawing layer.
    var shapeLayer: CAShapeLayer!

    // MARK: - Class Methods

    /// Animation function.
    func animate(period: CFTimeInterval) {
        let p: PathConfig = PathConfig(arcCenter: CGPoint(x: frame.width / 2, y: frame.height / 2) , radius: 17.5)
        let b: UIBezierPath = UIBezierPath(arcCenter: p.arcCenter, radius: p.radius, startAngle: p.startAngle, endAngle: p.endAngle, clockwise: true)
        let a: CABasicAnimation = CABasicAnimation(keyPath: "path")

        a.fromValue = shapeLayer.path
        a.toValue = b.cgPath
        a.autoreverses = true
        a.duration = period
        a.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        a.repeatCount = HUGE
        shapeLayer.add(a, forKey: "animateRadius")
    }

    /// Initializes and returns a CAShapeLayer with the given draw path and fill color.
    private class func shapeLayerForPath(_ path: CGPath, with fillColor: CGColor) -> CAShapeLayer {
        let shapeLayer: CAShapeLayer = CAShapeLayer()
        shapeLayer.path = path
        shapeLayer.fillColor = fillColor
        return shapeLayer
    }

    /// Configures the UIView
    private func setup() {

        // Init config, path.
        let cfg: PathConfig = PathConfig(arcCenter: CGPoint(x: frame.width / 2, y: frame.height / 2) , radius: 0.0)
        let path: UIBezierPath = UIBezierPath(arcCenter: cfg.arcCenter, radius: cfg.radius, startAngle: cfg.startAngle, endAngle: cfg.endAngle, clockwise: true)

        // Init CAShapeLayer
        shapeLayer = RGSStandbyIndicatorView.shapeLayerForPath(path.cgPath, with: UIColor.white.cgColor)

        layer.addSublayer(shapeLayer)
    }

    // MARK: Class Method Overrides

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

}

I've tried adjusting the start and end angles hoping it was some artifact of the path having no overlap, but it didn't change anything. I'm not sure how to fix this problem, and It's based off Chadwick Wood's answer to a similar question.

Upvotes: 1

Views: 2239

Answers (1)

Reinier Melian
Reinier Melian

Reputation: 20804

Use this UIView subclass

class RadioAnimationView: UIView {

    var animatableLayer : CAShapeLayer?

    override func awakeFromNib() {
        super.awakeFromNib()
        self.layer.cornerRadius = self.bounds.height/2

        self.animatableLayer = CAShapeLayer()
        self.animatableLayer?.fillColor = self.backgroundColor?.cgColor
        self.animatableLayer?.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.layer.cornerRadius).cgPath
        self.animatableLayer?.frame = self.bounds
        self.animatableLayer?.cornerRadius = self.bounds.height/2
        self.animatableLayer?.masksToBounds = true
        self.layer.addSublayer(self.animatableLayer!)
        self.startAnimation()
    }


    func startAnimation()
    {
        let layerAnimation = CABasicAnimation(keyPath: "transform.scale")
        layerAnimation.fromValue = 1
        layerAnimation.toValue = 3
        layerAnimation.isAdditive = false
        layerAnimation.duration = CFTimeInterval(2)
        layerAnimation.fillMode = kCAFillModeForwards
        layerAnimation.isRemovedOnCompletion = true
        layerAnimation.repeatCount = .infinity
        layerAnimation.autoreverses = true

        self.animatableLayer?.add(layerAnimation, forKey: "growingAnimation")
    }

}

Hope this helps you

Upvotes: 3

Related Questions