iOS - Pie progress bar with rounded corners - ios

I need to implement a progress bar according to this design :
As you can see, there is a corner radius to the bar itself.
This is how it looks now with my current code :
So, how to do that?
This is my current code:
- (void)animateProgressBarToPercent:(float)percent
{
if (percent > 1.0f) return;
int radius = 42.7f;
int strokeWidth = 7.f;
CGColorRef color = [UIColor someColor].CGColor;
int timeInSeconds = percent * 5;
CGFloat startAngle = 0;
CGFloat endAngle = percent;
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius) cornerRadius:radius].CGPath;
circle.position = CGPointMake(self.center.x - radius, self.center.y + strokeWidth);
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = color;
circle.lineWidth = strokeWidth;
circle.strokeEnd = endAngle;
[self.layer addSublayer:circle];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = timeInSeconds;
drawAnimation.repeatCount = 1.0;
drawAnimation.removedOnCompletion = NO;
drawAnimation.fromValue = [NSNumber numberWithFloat:startAngle];
drawAnimation.toValue = [NSNumber numberWithFloat:endAngle];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}

circle.lineCap = kCALineCapRound
Just set this property of your CAShapeLayer

Related

Animate CAGradientLayer while drawing or fill color

I am working on a circular progress view. I am adding slices or ARCs of a circle with unselected and selected colours that indicate progress of some task.
I am working in CALayers to draw Arcs and add animations. Everything is working fine for all slices with out gradient. If I add gradient in any slice / Arc it does not animate while drawing immediately.
Please help me in this issue to add animation in CAShapeLayer having CAGradientLayer.
Below is my code and my tried approaches...
NOTE: I have tried three approaches to add animation. Please check the commented code also
- (void)drawArcAnimation:(CGRect)rect {
if (self.itemIndex >= self.sliceItems.count) {
return;
}
float total = [self calculateTotal];
SliceItem *item = self.sliceItems[self.itemIndex];
UIBezierPath* path = [UIBezierPath bezierPath];
float angle = (item.itemValue/total) * 2 * M_PI;
float endAngle = self.startAngle + angle;
totalProgress += item.itemValue;
if (item.shouldGradient) {
// ***** This code will be used if we go for second approach of animation ****
UIBezierPath *rectToAnimateFrom = [UIBezierPath bezierPathWithArcCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2) radius:rect.size.width/2 - self.lineWidth startAngle:self.startAngle endAngle:self.startAngle clockwise:NO];
UIBezierPath *rectToAnimateTo = [UIBezierPath bezierPathWithArcCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2) radius:rect.size.width/2 - self.lineWidth startAngle:self.startAngle endAngle:endAngle clockwise:NO];
// ***** END ****
CGMutablePathRef arc = CGPathCreateMutable();
CGPathAddArc(arc, NULL,
(rect.size.width / 2), (rect.size.height / 2),
rect.size.width/2 - self.lineWidth,
self.startAngle,
endAngle,
NO);
self.startAngle = endAngle;
CGFloat lineWidth = self.lineWidth;
CGPathRef strokedArc =
CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
CAShapeLayer *segment = [CAShapeLayer layer];
segment.fillColor = item.itemColor.CGColor;
segment.strokeColor = [UIColor clearColor].CGColor;
segment.path = strokedArc;
// [self.baseLayer addSublayer:segment];
CAGradientLayer *gradient = [CAGradientLayer layer];
if (totalProgress > 50) {
gradient.colors = #[(id)item.itemSecondaryColor.CGColor, (id)item.itemColor.CGColor,(id)item.itemColor.CGColor,(id)item.itemColor.CGColor];
}else {
gradient.colors = #[(id)item.itemColor.CGColor, (id)item.itemColor.CGColor, (id)item.itemColor.CGColor,(id)item.itemSecondaryColor.CGColor];
}
gradient.frame = CGPathGetBoundingBox(segment.path);
CAShapeLayer *mask = [CAShapeLayer layer];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame),
-CGRectGetMinY(gradient.frame));
mask.path = CGPathCreateCopyByTransformingPath(segment.path,
&translation);
gradient.mask = mask;
[self.baseLayer addSublayer:gradient];
if (_duration == 0.0) {
self.itemIndex++;
[self drawArcAnimation:rect];
}else {
// ******* First Approach to animate *******
[CATransaction begin];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = _duration*(angle/(2 * M_PI));
drawAnimation.repeatCount = 1.0;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[CATransaction setCompletionBlock:^{
self.itemIndex++;
[self drawArcAnimation:rect];
}];
[gradient addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
[CATransaction commit];
// ******* Second Approach to animate *******
// [CATransaction begin];
// CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
// animation.fromValue = (__bridge id _Nullable)(rectToAnimateFrom.CGPath);
// animation.toValue = (__bridge id _Nullable)(rectToAnimateTo.CGPath);
// animation.duration = 3;
// animation.repeatCount = 1;
// animation.removedOnCompletion = false;
// animation.fillMode = kCAFillModeForwards;
//
// [CATransaction setCompletionBlock:^{
// self.itemIndex++;
// [self drawArcAnimation:rect];
// }];
//
// [gradient addAnimation:animation forKey:#"fill animation"];
// [CATransaction commit];
// ******* Third Approach to animate *******
// NSArray *fromColors = #[(id)UIColor.whiteColor.CGColor, (id)UIColor.clearColor.CGColor];
// NSArray *toColors = gradient.colors;
// [gradient setColors:toColors];
//
// [CATransaction begin];
// CABasicAnimation *animation1 = [CABasicAnimation animationWithKeyPath:#"colors"];
// animation1.fromValue = fromColors;
// animation1.toValue = toColors;
// animation1.duration = _duration;
// animation1.removedOnCompletion = YES;
// animation1.fillMode = kCAFillModeForwards;
// animation1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// [CATransaction setCompletionBlock:^{
// self.itemIndex++;
// [self drawArcAnimation:rect];
// }];
// [gradient addAnimation:animation1 forKey:#"animateGradient"];
// [CATransaction commit];
}
}else {
[path addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
radius:rect.size.width/2 - self.lineWidth
startAngle:self.startAngle
endAngle:endAngle
clockwise:YES];
self.startAngle = endAngle;
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
CGRect frame = path.bounds;
frame.size.height = rect.size.height;
frame.size.width = frame.size.width * 30;
layer.strokeColor = item.itemColor.CGColor;
layer.fillColor = nil;
layer.lineWidth = self.lineWidth;
layer.strokeStart = 0;
layer.strokeEnd = 1.0;
[self.baseLayer addSublayer:layer];
if (_duration == 0.0) {
self.itemIndex++;
[self drawArcAnimation:rect];
}else {
[CATransaction begin];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = _duration*(angle/(2 * M_PI));
animation.fromValue = #(0.0);
animation.toValue = #(1.0);
[CATransaction setCompletionBlock:^{
self.itemIndex++;
[self drawArcAnimation:rect];
}];
[layer addAnimation:animation forKey:nil];
[CATransaction commit];
}
}
}
In above code you can a condition if (item.shouldGradien) and its else part. Animations are working fine in else part of CALayers but no in gradient part.
What I am doing wrong here? Any help... ???

Animate progress with filling the color of the border width of a UIImageView layer

I have am UIImageView and I have made it to be circle with a width layer like in this image :
User can update the image and upload new one, I have a progress call back while image is uploaded. What I want is to animate the border with color while the image is uploaded, for example when user click upload the border start from top with green color and fill the width according to the progress.
I tried with this code:
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointZero radius:27 startAngle:-M_PI_2 endAngle:2 * M_PI - M_PI_2 clockwise:YES].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor greenColor].CGColor;
circle.lineWidth = 4;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 10;
animation.removedOnCompletion = NO;
animation.fromValue = #(0);
animation.toValue = #(1);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circle addAnimation:animation forKey:#"drawCircleAnimation"];
[imageView.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
[imageView.layer addSublayer:circle];
But It shows the circle in wrong position and this aproch does not take a progress, it's static is there any way to fill the border width of the layer of UIImageView according to progress ?
UIImageView is meant to show the images. I would not suggest changing it's behaviour. Even though you subclass the UIImageView and try to draw something in it, the drawRect of it will not get called. Let the imageView be imageView and use UIView for the rest
I suggest you to subclass the UIView and then draw a bezier path in it and animate the bezier path curve, You can later add the imageview to the uiview .
UIView subclass:
- (void)drawRect:(CGRect)rect {
[self drawImageHolderViewFrame:rect startAngle:_startAngle];
}
- (void)drawImageHolderViewFrame: (CGRect)frame startAngle: (CGFloat)startAngle
{
CGRect ovalRect = CGRectMake(CGRectGetMinX(frame) , CGRectGetMinY(frame) , frame.size.width-10, frame.size.height-10);
UIBezierPath* ovalPath = [UIBezierPath bezierPath];
[ovalPath addArcWithCenter: CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)) radius: CGRectGetWidth(ovalRect) / 2 startAngle: -91 * M_PI/180 endAngle: -startAngle * M_PI/180 clockwise: YES];
[UIColor.redColor setStroke];
ovalPath.lineWidth = 2;
[ovalPath stroke];
}
viewController class:
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
_imageHolderView=[[MyView alloc]initWithFrame:CGRectMake(20, 20, 100, 100)];
_imageHolderView.startAngle=90;
_imageHolderView.backgroundColor=[UIColor clearColor];
[self.view addSubview: _imageHolderView];
[self startTImer];
}
-(void)startTImer{
NSTimer *timer=[NSTimer scheduledTimerWithTimeInterval:0.02 target:self selector:#selector(animateBorder) userInfo:nil repeats:YES];
}
-(void)animateBorder{
if (_startAngle<=-270) {
_startAngle=90;
}
_startAngle--;
_imageHolderView.startAngle=_startAngle;
[_imageHolderView setNeedsDisplay];
}
This would give you:
Now you can add the imageview as a subview to the view you just created.
Please try the code below:
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(29, 29) radius:27 startAngle:-M_PI_2 endAngle:2 * M_PI - M_PI_2 clockwise:YES].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor greenColor].CGColor;
circle.lineWidth = 4;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 10;
animation.removedOnCompletion = NO;
animation.fromValue = #(0);
animation.toValue = #(1);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[circle addAnimation:animation forKey:#"drawCircleAnimation"];
[imageCircle.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
[imageCircle.layer addSublayer:circle];

How to animate a curve

I would like to change the curve's Y position which acts like a UISlider with animation.
I have a method drawCurve that will draw a quadCurve, but I would like to animate it. I'm not too sure how I should link it up with CABasicAnimation.
//draw line path
-(void)drawCurve
{
//ensure the drag up or down just in the middle
if (controlPoint.x <= halfWidth || controlPoint.x >= halfWidth) {controlPoint.x = halfWidth;}
if (controlPoint.y <= 0 ) {controlPoint.y = 0;}
if (controlPoint.y >= self.frame.size.height) {controlPoint.y = self.frame.size.height;}
CGContextRef ctx = UIGraphicsGetCurrentContext();
curvePath = CGPathCreateMutable();
CGPathMoveToPoint(curvePath, NULL, startingDrawPointX, startingDrawPointY);
CGPathAddQuadCurveToPoint(curvePath, NULL, controlPoint.x, controlPoint.y, endingDrawPointX, endingDrawPointY);
CGContextAddPath(ctx, curvePath);
CGContextSetStrokeColorWithColor(ctx,kDBrownTextColor.CGColor);
CGContextSetLineWidth(ctx, 2.0);//thickness
CGContextStrokePath(ctx);
CGPathRelease(curvePath);
}
-(void)startAnimation
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 1;
animation.repeatCount = 5;
animation.autoreverses = NO;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (__bridge id)currentCurvePath;
animation.toValue = (__bridge id)ToPath;
[self.layer.superlayer addAnimation:animation forKey:#"animatePath"];
}
Here is an example of what I think your trying to accomplish, using a CAShapeLayer.
+(Class) layerClass {
return [CAShapeLayer class];
}
//draw line path
-(void)drawCurve
{
//ensure the drag up or down just in the middle
if (controlPoint.x <= halfWidth || controlPoint.x >= halfWidth) {controlPoint.x = halfWidth;}
if (controlPoint.y <= 0 ) {controlPoint.y = 0;}
if (controlPoint.y >= self.frame.size.height) {controlPoint.y = self.frame.size.height;}
//create the path
curvePath = CGPathCreateMutable();
CGPathMoveToPoint(curvePath, NULL, startingDrawPointX, startingDrawPointY);
CGPathAddQuadCurveToPoint(curvePath, NULL, controlPoint.x, controlPoint.y, endingDrawPointX, endingDrawPointY);
//add the path to the background layer
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
layer.path = curvePath;
layer.strokeColor = [UIColor brownColor].CGColor;
layer.lineWidth = 2.0;
//release the path
CGPathRelease(curvePath);
}
-(void)startAnimation {
[CATransaction begin];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 1;
animation.repeatCount = 5;
animation.autoreverses = NO;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (__bridge id)curvePath;
animation.toValue = (__bridge id)ToPath;
[self.layer addAnimation:animation forKey:#"animatePath"];
[CATransaction commit];
}

How do I correctly position an animated CAShapeLayer

I'm animating the end stroke of a CAShapeLayer, but the position is incorrect and I can't figure out what I'm doing wrong. The frame is correct, but when I try and adjust the position I can't get it to "fill" the frame's width of the view. I've tried to adjust the _maskLine's position, but my approaches have failed thus far.
The frame of the view is: 10, 10, 50, 100
See image (the red shape layer should be "shifted" over 10 pts.
// layer setup in initWithFrame
_maskLine = [CAShapeLayer layer];
_maskLine.lineCap = kCALineCapButt;
_maskLine.fillColor = [[UIColor whiteColor] CGColor];
_maskLine.lineWidth = frame.size.width;
_maskLine.strokeEnd = 0.0;
_maskLine.frame = self.bounds;
[self.layer addSublayer:_maskLine];
self.clipsToBounds = YES;
self.layer.cornerRadius = 2.0f;
// animation method logic
UIBezierPath *progressline = [UIBezierPath bezierPath];
NSUInteger randomInt = arc4random() % (NSUInteger)self.maxMeterHeight;
self.meterHeight = (randomInt / self.maxMeterHeight);
[progressline moveToPoint:CGPointMake(CGRectGetMidX(self.frame), CGRectGetHeight(self.frame))];
[progressline addLineToPoint:CGPointMake(CGRectGetMidX(self.frame), (1 - self.meterHeight) * CGRectGetHeight(self.frame))];
[progressline setLineWidth:1.0];
[progressline setLineCapStyle:kCGLineCapSquare];
self.maskLine.path = progressline.CGPath;
self.maskLine.strokeColor = [UIColor redColor].CGColor;
self.maskLine.frame = self.bounds;
NSLog(#"mask frame: %#", NSStringFromCGRect(self.maskLine.frame));
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 0.35;
pathAnimation.autoreverses = YES;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
pathAnimation.fromValue = #0.0f;
pathAnimation.toValue = #1.0f;
pathAnimation.cumulative = YES;
[self.maskLine addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
[CATransaction commit];
self.maskLine.strokeEnd = 1.0;
The "fix" was changing these lines
[progressline moveToPoint:CGPointMake(CGRectGetMidX(self.frame), CGRectGetHeight(self.frame))];
[progressline addLineToPoint:CGPointMake(CGRectGetMidX(self.frame), (1 - self.meterHeight) * CGRectGetHeight(self.frame))];
to:
[progressline moveToPoint:CGPointMake(self.frame.size.width / 2, CGRectGetHeight(self.frame))];
[progressline addLineToPoint:CGPointMake(self.frame.size.width / 2, (1 - self.meterHeight) * CGRectGetHeight(self.frame))];

Re-draw UIBezierPath based on random CGFloat value

I am developing a custom circular progress bar using UIBezierPath. I want to change the progress based on randomly generated CGFloat values.
My progress bar looks like this:
I drew the progress bar using the following code:
// Draw the arc with bezier path
int radius = 100;
CAShapeLayer *arc = [CAShapeLayer layer];
arc.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 50) radius:radius startAngle:M_PI endAngle:M_PI/150 clockwise:YES].CGPath;
arc.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
arc.cornerRadius = 100.0;
arc.fillColor = [UIColor clearColor].CGColor;
arc.strokeColor = [UIColor purpleColor].CGColor;
arc.lineWidth = 6;
[self.view.layer addSublayer:arc];
// Animation of the progress bar
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = YES; // Remain stroked after the animation..
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:10.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[arc addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
// Gradient of progress bar
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.view.frame;
gradientLayer.colors = #[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor purpleColor].CGColor ];
gradientLayer.startPoint = CGPointMake(0,0.1);
gradientLayer.endPoint = CGPointMake(0.5,0.2);
[self.view.layer addSublayer:gradientLayer];
gradientLayer.mask = arc;
refreshButton = [[UIButton alloc] initWithFrame:CGRectMake(50, 370, 50, 50)];
[refreshButton addTarget:self action:#selector(refresh:) forControlEvents:UIControlEventTouchUpInside];
[refreshButton setBackgroundImage:[UIImage imageNamed:#"refresh.png"] forState:UIControlStateNormal];
[self.view addSubview:refreshButton];
The UIButton with the green arrow generates randomly CGFloat values. So my question is how to reset the animation so that it stops at the corresponding position if it is between 0.0 and 1.0? I tried to insert the code for the animation inside the method of the button, but as a result the new arc was drawn on top of the existing one.
-(void)refresh:(id)sender{
NSLog(#"Refresh action:");
CGFloat randomValue = ( arc4random() % 256 / 256.0 );
NSLog(#"%f", randomValue);
valueLabel.text = #"";
valueLabel = [[UILabel alloc] initWithFrame:CGRectMake(150, 370, 100, 50)];
valueLabel.text = [NSString stringWithFormat: #"%.6f", randomValue];
[self.view addSubview:valueLabel];
// Draw the arc with bezier path
int radius = 100;
CAShapeLayer *arc = [CAShapeLayer layer];
arc.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 50) radius:radius startAngle:M_PI endAngle:M_PI/150 clockwise:YES].CGPath;
arc.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
arc.cornerRadius = 100.0;
arc.fillColor = [UIColor clearColor].CGColor;
arc.strokeColor = [UIColor purpleColor].CGColor;
arc.lineWidth = 6;
[self.view.layer addSublayer:arc];
// Animation of the progress bar
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = YES; // Remain stroked after the animation..
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:randomValue];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[arc addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
I have no clue how to re-draw the arc, so any help or idea would be greatly appreciated!
Thanks a lot.
Granit
Try re-using the sublayer or replacing it. Make arc a class variable. Then cycle through the sublayers, remove the old arc then remake it with your new random float percentage. Once arc is a class variable then you can either replace it and animate from zero or you can animate from it's current percent to a new percent.

Resources