This question already has an answer here:
- Draw part of a circle 8 answers
- iPhone Core Animation - Drawing a Circle 4 answers
- iOS: How to draw a circle step by step with NSTimer 2 answers
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.
iOS-CAAnimation-group-demo
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.
So I took what Duncan C had in his linked GitHub project (in Objective-C) and applied it to my scenario (also converted it to Swift) - and it works great!
Here's the final solution:
Thanks, Duncan C - that was super helpful.
Here is an example of a progress circle using your suggestion. The view draws a progress circle that animates over time, starting out as a full circle and erasing itself to reveal the view behind the circle. There are
@IBInspectable
properties that can be modified to fill the circle instead of erasing.Here is a link to the project. The view is in
CircleTimer.view
. If you run the project, you can enter a number of seconds and see the circle erase over the specified time.https://github.com/riley2012/circle-timer-ios
This is the method that does the animation:
}