I want to make a circular progress bar such as that, how to animate its resizing and properties (shape, colour, width, etc.) as well.
I am trying to make it around a circular transparent view.
Where to start?
Does anyone has a sample code?
There's no substitute for learning, however a little help never goes a miss, so here are snippets of code that will accomplish the task for you.
The concept is to use a CAShapeLayer and a UIBezierPath and progress is simply setting the strokeEnd propertie of the UIBezierPath. You'll need to declare a CAShapeLayer and set its properties. We'll call this our progressLayer. (i'm not going to provide complete code, simply direction and samples for you to put together.)
// setup progress layer
// N.B borderWidth is a float representing a value used as a margin.
// pathwidth is the width of the progress path
// obviously progressBounds is a CGRect specifying the Layer's Bounds
[self.progressLayer setFrame:progressBounds];
UIBezierPath *progressPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)) radius:(bounds.size.height - self.borderWidth - self.pathWidth ) / 2 startAngle: (5* -M_PI / 12) endAngle: (2.0 * M_PI - 7 * M_PI /12) clockwise:YES];
self.progressLayer.strokeColor = [UIColor whiteColor].CGColor;
self.progressLayer.lineWidth = 6.0f ;
self.progressLayer.path = progressPath.CGPath;
self.progressLayer.anchorPoint = CGPointMake(0.5f, 0.5f);
self.progressLayer.fillColor = [UIColor clearColor].CGColor;
self.progressLayer.position = CGPointMake(self.layer.frame.size.width / 2 - self.borderWidth / 2, self.layer.frame.size.height / 2 - self.borderWidth/2);
[self.progressLayer setStrokeEnd:0.0f];
You will obviously need to add progressLayer to your view hierarchie
Then you will need a simple animation to progress the bar;
// updateInterval is a float specifying the duration of the animation.
// progress is a float storing the, well, progress.
// newProgress is a float
[self.progressLayer setStrokeEnd:newProgress];
CABasicAnimation *strokeEndAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
strokeEndAnimation.duration = updateInterval;
[strokeEndAnimation setFillMode:kCAFillModeForwards];
strokeEndAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
strokeEndAnimation.removedOnCompletion = NO;
strokeEndAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
strokeEndAnimation.toValue = [NSNumber numberWithFloat:newProgress];
self.progress = newProgress;
[self.progressLayer addAnimation:strokeEndAnimation forKey:#"progressStatus"];
in your image above, the un-progressed path is nothing more than a second fully stroked layer behind the progressLayer
oh, and one final point, you'll find that the Circle progresses Clockwise. If you take the time to learn what's happening here, you'll figure out how to set progress Anti Clockwise.
Good Luck ...
You can also check out my lib MBCircularProgressBar, it also shows how to animate with and how to make it customizable from the Interface Builder. You can make it transparent by using transparent colors.
http://raw.github.com/matibot/MBCircularProgressBar/master/Readme/example.jpg
Here's a great, short tutorial on how to make this: http://www.tommymaxhanks.com/circular-progress-view/
You will need to learn a bit of Core Graphics in order to accomplish this in the way you want (the author used a gradient in the implementation, you may want to change that.), but it's worth it if you want to do cool stuff like this on the iPhone!
I think even more helpful is the example code, which is hosted here: https://github.com/mweyamutsvene/THControls/tree/master/CircularProgressView
Related
I am running into an issue when I create an explicit animation to change the value of a CAShapeLayer's path from an ellipse to a rect.
In my canvas controller I setup a basic CAShapeLayer and add it to the root view's layer:
CAShapeLayer *aLayer;
aLayer = [CAShapeLayer layer];
aLayer.frame = CGRectMake(100, 100, 100, 100);
aLayer.path = CGPathCreateWithEllipseInRect(aLayer.frame, nil);
aLayer.lineWidth = 10.0f;
aLayer.strokeColor = [UIColor blackColor].CGColor;
aLayer.fillColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:aLayer];
Then, when I animate the path I get a strange glitch / flicker in the last few frames of the animation when the shape becomes a rect, and in the first few frames when it animates away from being a rect. The animation is set up as follows:
CGPathRef newPath = CGPathCreateWithRect(aLayer.frame, nil);
[CATransaction lock];
[CATransaction begin];
[CATransaction setAnimationDuration:5.0f];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"path"];
ba.autoreverses = YES;
ba.fillMode = kCAFillModeForwards;
ba.repeatCount = HUGE_VALF;
ba.fromValue = (id)aLayer.path;
ba.toValue = (__bridge id)newPath;
[aLayer addAnimation:ba forKey:#"animatePath"];
[CATransaction commit];
[CATransaction unlock];
I have tried many different things like locking / unlocking the CATransaction, playing with various fill modes, etc...
Here's an image of the glitch:
http://www.postfl.com/outgoing/renderingglitch.png
A video of what I am experiencing can be found here:
http://vimeo.com/37720876
I received this feedback from the quartz-dev list:
David Duncan wrote:
Animating the path of a shape layer is only guaranteed to work when
you are animating from like to like. A rectangle is a sequence of
lines, while an ellipse is a sequence of arcs (you can see the
sequence generated by using CGPathApply), and as such the animation
between them isn't guaranteed to look very good, or work well at all.
To do this, you basically have to create an analog of a rectangle by
using the same curves that you would use to create an ellipse, but
with parameters that would cause the rendering to look like a
rectangle. This shouldn't be too difficult (and again, you can use
what you get from CGPathApply on the path created with
CGPathAddEllipseInRect as a guide), but will likely require some
tweaking to get right.
Unfortunately this is a limitation of the otherwise awesome animatable path property of CAShapeLayers.
Basically it tries to interpolate between the two paths. It hits trouble when the destination path and start path have a different number of control points - and curves and straight edges will have this problem.
You can try to minimise the effect by drawing your ellipse as 4 curves instead of a single ellipse, but it still isn't quite right. I haven't found a way to go smoothly from curves to polygons.
You may be able to get most of the way there, then transfer to a fade animation for the last part - this won't look as nice, though.
I am working on a kids letter tracing app - see attached screen shot.
I was able to display the letter by using the bezierpath identified by the font glyphs and allow writing the touches inside the bezierpath.
Now, i want to add an help animation so that it shows how to write this letter from start to finish.
How to do that?
Consider we display "A" and i want to show animation shows how to write "A" for kids.
Any pointers / ideas.
thanks much.
I used this animation for my app. Created a circular progress bar that fills up dot clockwise
CABasicAnimation *pathAnim = [CABasicAnimation animationWithKeyPath: #"path"];
pathAnim.toValue = (id)newPath.CGPath;
// -- initialize CAAnimation group with duration of 0.2secs and beginTime will begin after another.
CAAnimationGroup *anims = [CAAnimationGroup animation];
anims.animations = [NSArray arrayWithObjects:pathAnim, nil];
anims.removedOnCompletion = NO;
anims.duration = 0.2f;
anims.beginTime = CACurrentMediaTime() + anims.duration*i;
anims.fillMode = kCAFillModeForwards;
[anims setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[progressLayer addAnimation:anims forKey:nil];
There is no straightforward way to do this. I imagine the following would be easiest to implement:
Create a path per character that will trace how the stroke will be written. If you have limited number of characters like in the alphabet, it's possible to do this by hand. You can implement something like this to capture the path: http://code.tutsplus.com/tutorials/smooth-freehand-drawing-on-ios--mobile-13164
Use your current path (in red in your photo) as a clipping mask so the fill does not overflow.
Stroke the path you created in 1. with a very thick stroke to do the fill.
I can't find a "built-in" way to animate the fill as you want. There is a built-in way to animate the stroke, however:
Use a UIView with a CAShapeLayer backing it. CAShapeLayer has a path property. You can then apply an animation to the layer's strokeStart and/or strokeEnd properties.
Hope it helps.
After loading your bezierpath add init the animation:
CAKeyframeAnimation *shapeKeyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:#"strokeEnd"];
shapeKeyframeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
shapeKeyframeAnimation.keyTimes = #[#0.0f, #0.6f, #1.0f];
shapeKeyframeAnimation.values = #[#0.0f, #1.0f, #1.0f];
shapeKeyframeAnimation.duration = 4.0f;
shapeKeyframeAnimation.repeatCount = CGFLOAT_MAX;
then add it to your shape layer:
[shapeLayer addAnimation:_loadingKeyframeAnimation forKey:#"stoke"];
I have been searching the web for a library\solution how to achieve this.
I need to make a circular progress bar.
The progress bar should be built from up to 3 colors, depending of the progress value.
I have found several circular progress bars but I'm having trouble modifying them to the required result.
It's supposed to look sort of like the following image, only with a value label in the centre.
Circular gradient
When setting the progress, it should animate to that point.
Each color is up to a certain progress and then it should change with gradient to the next one.
Any ideas on how to achieve this?
I use a progress HUD from github here. It has an annular progress display.
I used the drawing code from this and had a play making a few adjustments. The following code will draw you an annular ring with each part of the ring using a color based on the progress. Code assumes the progress is in self.progress.
This code draws an annular ring in steps of 0.05 progress. So that gives you 20 color changes possible. Reduce this to show more and increase to show less. This code changes the ring color from red at 0.0 progress to green at 1.0 progress as a demonstration. Just change the code which sets the color to an algorithm of your choosing, table lookup etc...
It works when I do the changes inside the HUD linked to above. Just put this function in the view you want to draw the annular ring in.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat lineWidth = 5.f;
CGPoint center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
CGFloat radius = (self.bounds.size.width - lineWidth)/2;
CGFloat startAngle = - ((float)M_PI / 2); // 90 degrees
CGFloat endAngle = (2 * (float)M_PI) + startAngle;
UIBezierPath *processPath = [UIBezierPath bezierPath];
processPath.lineCapStyle = kCGLineCapRound;
processPath.lineWidth = lineWidth;
endAngle = (self.progress * 2 * (float)M_PI) + startAngle;
CGFloat tmpStartAngle=startAngle;
CGFloat shownProgressStep=0.05; // The size of progress steps to take when rendering progress colors.
for (CGFloat shownProgress=0.0;shownProgress <= self.progress ;shownProgress+=shownProgressStep){
endAngle=(shownProgress * 2 *(float)M_PI) + startAngle;
CGFloat rval=shownProgress;
CGFloat gval=(1.0-shownProgress);
UIColor *progressColor=[UIColor colorWithRed:rval green:gval blue:0.0 alpha:1.0];
[progressColor set];
[processPath addArcWithCenter:center radius:radius startAngle:tmpStartAngle endAngle:endAngle clockwise:YES];
[processPath stroke];
[processPath removeAllPoints];
tmpStartAngle=endAngle;
}
}
Most probably you will have to go all the way down to CoreGraphics. You can find some instructions on how to that here:
Draw segments from a circle or donut
I've got a project where I'm animating a UIBezierPath based on a set progress. The BezierPath is in the shape of a circle and lies in a UIView and animation is done in drawRect using CADisplayLink right now. Simply put, based on a set progress x the path should radially extend (if xis larger than before) or shrink (if x is smaller).
self.drawProgress = (self.displayLink.timestamp - self.startTime)/DURATION;
CGFloat startAngle = -(float)M_PI_2;
CGFloat stopAngle = ((self.x * 2*(float)M_PI) + startAngle);
CGFloat currentEndAngle = ((self.oldX * 2*(float)M_PI) + startAngle);
CGFloat endAngle = currentEndAngle-((currentEndAngle-stopAngle)*drawProgress);
UIBezierPath *guideCirclePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
This is in the case of x shrinking since our last update. The issues I'm experiencing are actually a few:
The shape always starts drawing at 45º (unless I rotate the view). I have not found any way to change this, and setting the startAngleto -45º makes no difference really because it always "pops" to 45. Is there anything I can do about this, or do I have to resort to other methods of drawing?
Is there any other way that one should animate these things? I've read much about using CAShapeLayer but I haven't quite understood the actual difference (in terms of drawbacks and benefits) in using these two methods. If anyone could clarify I would be very much obliged!
UPDATE: I migrated the code over to CAShapeLayer instead, but now I'm facing a different issue. It's best described with this image:
What's happening is that when the layer is supposed to shrink, the thin outer line is still there (regardless of direction of movement). And when the bar shrinks, the delta of 1-xisn't removed unless I explicitly make a new white shape over it. The code for this follows. Any ideas?
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:stopAngle clockwise:YES];
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [circlePath CGPath];
circle.strokeStart = 0;
circle.strokeEnd = 1.0*self.progress;
// Colour and other customizations here.
if (self.progress > self.oldProgress) {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(circle.strokeEnd);
} else {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(1.0*self.progress);
circle.strokeEnd = 1.0*self.progress;
}
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; //kCAMediaTimingFunctionEaseIn
[circle addAnimation:drawAnimation forKey:#"strokeEnd"];
UPDATE 2: I've ironed out most of the other bugs. Turned out it was just me being rather silly the whole time and overcomplicating the whole animation (not to mention multiplying by 1 everywhere, what?). I've made a gif of the bug I can't solve:
Any ideas?
UPDATE 3: (and closure). I managed to get rid of the bug by calling
[self.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
And now everything works as it should. Thanks for all the help!
Using CAShapeLayer is much easier and cleaner. The reason is that CAShapeLayer includes properties strokeStart and strokeEnd. These values range from 0 (the beginning of the path) to 1 (the end of the path) and are animatable.
By changing them you can easily draw any arc of your circle (or any part of an arbitrary path, for that matter.) The properties are animatable, so you can create an animation of a growing/shrinking pie slice or section of a ring shape. It's much easier and more performant than implementing code in drawRect.
Using the code below I've been trying to get my CALayer to scale around its center - but it scales by left/top (0,0). I saw this question: Anchor Point in CALayer and have tried setting the anchorPoint - but it was [.5, .5] before I set it and setting makes no difference. Also tried setting contentsGravity.
The set up is I have a CAShapeLayer added to self.view.layer I do some drawing in it and then add the CABasicAnimation. The animation runs fine - but it scales toward the top/left - not the center of the view.
Have been tinkering with this for a few hours - it must be something simple but I'm not getting it.
shapeLayer.anchorPoint = CGPointMake(.5,.5);
shapeLayer.contentsGravity = #"center";
printf("anchorPoint %f %f\n", shapeLayer.anchorPoint.x, shapeLayer.anchorPoint.y);
CABasicAnimation *animation =
[CABasicAnimation animationWithKeyPath:#"transform.scale"];
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
[animation setDuration:1.0];
animation.fillMode=kCAFillModeForwards;
animation.removedOnCompletion=NO;
[shapeLayer addAnimation:animation forKey:#"zoom"];
What is the bounds of your layer?
I bet it has zero size; try setting its size to the same size as your shape.
CAShapeLayer draws the shape as kind of an overlay, independent of the contents (and bounds and contentsGravity and etc.) that you'd use in an ordinary CALayer. It can be a bit confusing.
Also make sure that you're adding the animation after the view is created. If you're trying to add animation in the viewDidLoad, it probably won't work. Try adding it to viewDidAppear instead.
I only know Swift, but here's an example:
override func viewDidAppear (animated: Bool) {
addPulsation(yourButton)
}
func addPulsation (button: UIButton) {
let scaleAnimation:CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 1.0
scaleAnimation.repeatCount = 100
scaleAnimation.autoreverses = true
scaleAnimation.fromValue = 1.05;
scaleAnimation.toValue = 0.95;
button.layer.addAnimation(scaleAnimation, forKey: "scale")
}
It's an old thread but in case this helps anyone.
This ia a coordinate space problem. You just need to set the drawing space and the space of the the path. This code works for me...
...just put this at the start of the animation creation method (inside a method on the parent view)...
// add animation to remap (without affecting other elements)
let animationLayer = CALayer()
layer?.addSublayer(animationLayer). // (don't use optional for iOS)
// set the layer origin to the center of the view (or any center point for the animation)
animationLayer.bounds.origin .x = -self.bounds.size.width / 2.0
animationLayer.bounds.origin .y = -self.bounds.height / 2.0
// make image layer with circle around the zero point
let imageLayer = CAShapeLayer()
animationLayer.addSublayer(imageLayer)
let shapeBounds = CGRect(x: -bounds.size.width/2.0,
y:-bounds.size.height/2.0,
width: bounds.size.width,
height: bounds.size.height)
imageLayer.path = CGPath(ellipseIn: shapeBounds, transform: nil)
imageLayer.fillColor = fillColour
// **** apply your animations to imageLayer here ****
this code works unchanged on iOS and MacOS - apart from the optional needed on layer for MacOS.
(BTW you need to remove the animation layer after the animation finishes!)
I spent hours to find out how to do it according the center but couldn't find any solutions for OSX. I decided to to move layer using CATransform3DMakeTranslation and combine both transforms.
My code:
NSView *viewToTransform = self.view;
[viewToTransform setWantsLayer:YES];
viewToTransform.layer.sublayerTransform = CATransform3DConcat(CATransform3DMakeScale(-1.0f, -1.0f, 1.0f), CATransform3DMakeTranslation(viewToTransform.bounds.size.width, viewToTransform.bounds.size.height, 0));