I am trying to animate the paths of a CAShapeLayer so that I get the effect of a circle "filling" up to a specific amount.
The Issue
It "works", but is not AS smooth as I think it could be, and I want to introduce some easing to it. But because I'm animating each % individually, I'm not sure that's possible right now. So I'm looking for alternatives that could solve this issue!
Visual Explanation
To start -- here are some "frames" of the animation that I'm trying to achieve (see images - circle fills from 0% to 25%)
Code
I create the green stroke (outside):
let ovalPath = UIBezierPath(ovalInRect: CGRectMake(20, 20, 240, 240))
let circleStrokeLayer = CAShapeLayer()
circleStrokeLayer.path = ovalPath.CGPath
circleStrokeLayer.lineWidth = 20
circleStrokeLayer.fillColor = UIColor.clearColor().CGColor
circleStrokeLayer.strokeColor = colorGreen.CGColor
containerView.layer.addSublayer(circleStrokeLayer)
I create the initial path of the filled shape (inside):
let filledPathStart = UIBezierPath(arcCenter: CGPoint(x: 140, y: 140), radius: 120, startAngle: startAngle, endAngle: Math().percentToRadians(0), clockwise: true)
filledPathStart.addLineToPoint(CGPoint(x: 140, y: 140))
filledLayer = CAShapeLayer()
filledLayer.path = filledPathStart.CGPath
filledLayer.fillColor = colorGreen.CGColor
containerView.layer.addSublayer(filledLayer)
I then animate with a for loop and array of CABasicAnimations.
var animations: [CABasicAnimation] = [CABasicAnimation]()
func animate() {
for val in 0...25 {
let morph = CABasicAnimation(keyPath: "path")
morph.duration = 0.01
let filledPathEnd = UIBezierPath(arcCenter: CGPoint(x: 140, y: 140), radius: 120, startAngle: startAngle, endAngle: Math().percentToRadians(CGFloat(val)), clockwise: true)
filledPathEnd.addLineToPoint(CGPoint(x: 140, y: 140))
morph.delegate = self
morph.toValue = filledPathEnd.CGPath
animations.append(morph)
}
applyNextAnimation()
}
func applyNextAnimation() {
if animations.count == 0 {
return
}
let nextAnimation = animations[0]
animations.removeAtIndex(0)
filledLayer.addAnimation(nextAnimation, forKey: nil)
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
filledLayer.path = (anim as! CABasicAnimation).toValue as! CGPath
applyNextAnimation()
}
Don't try to create a whole bunch of separate animations. CAShapeLayer has a strokeStart and strokeEnd property. Animate that.
The trick is to install an arc of the whole circle in the shape layer, then create a CABasicAnimation that animates the shapeEnd from 0 to 1 (to animate the fill from 0% to 100%) or whatever values you need.
You can apply whatever timing you want on that animation.
I have a project (In Objective-C, I'm afraid) on Github that includes a "clock wipe" animation using this technique. Here's how it looks:
(That's a gif, which looks a little rough. The actual iOS animation is quite smooth.)
The link is below. Look for the "clock wipe" animation in the readme.
The clock wipe animation installs the shape layer as a mask on an image view's layer. You can instead draw your shape layer directly if that's what you want to do.