Fill Circular Ring with gradient color according to percentage.
core-animation, ios, ios7, objective-c
Solution
To fill an amount of the circle with color, you have to draw an outline on top of the filled layer and clip only the filled layer.
In order to make the gradient follow the circle, you will have to divide it into multiple gradients that are piecewise linear. since the graphic you have provided already has some segments with outlines, we can use these outlines to hide any imperfections from joining different gradients.
I made an example how this could work, if you want to have more control of the colors, you can set them for every segment in `CircleGradientLayer`. Control the drawn progress by setting the `progress` property of `CircleProgressView`.
Drawing the outlines could be done easier with two circles and some lines if you see my answer here: https://stackoverflow.com/a/24580817/3659846 but while writing this code, copy&pasting was faster ;)
nothing special in the .h files just insert what the compiler wants to have, I'm posting only the .m here:
CircleProgressView
@implementation CircleProgressView{
CircleGradientLayer *_gradientLayer;
CircleOutlineLayer *_outlineLayer;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
int numSegments = 7;
CGFloat circleRadius = 130;
CGFloat circleWidth = 30;
NSDictionary *circleData = @{
@"numberOfSegments":@(numSegments),
@"circleRadius":@(circleRadius),
@"circleWidth":@(circleWidth)
};
_gradientLayer = [CircleGradientLayer layer];
_gradientLayer.contentsScale = 2;
_gradientLayer.frame = self.bounds;
[_gradientLayer setCircleData:circleData];
[_gradientLayer setNeedsDisplay];
[self.layer addSublayer:_gradientLayer];
_outlineLayer = [CircleOutlineLayer layer];
_outlineLayer.frame = self.bounds;
_outlineLayer.contentsScale = 2;
[_outlineLayer setCircleData:circleData];
[_outlineLayer setNeedsDisplay];
[self.layer addSublayer:_outlineLayer];
self.progress = 1;
}
return self;
}
- (void)setProgress:(CGFloat)progress{
_progress = MAX(0, MIN(1, progress));
_gradientLayer.progress = progress;
}
@end
CircleGradientLayer
@implementation CircleGradientLayer{
int _numSegments;
CGFloat _circleRadius;
CGFloat _circleWidth;
CAShapeLayer *_maskLayer;
}
+(id)layer{
CircleGradientLayer *layer = [[CircleGradientLayer alloc] init];
return layer;
}
-(void)setCircleData:(NSDictionary*)data{
_numSegments = [data[@"numberOfSegments"] intValue];
_circleRadius = [data[@"circleRadius"] doubleValue];
_circleWidth = [data[@"circleWidth"] doubleValue];
[self createMask];
}
- (void)createMask{
_maskLayer = [CAShapeLayer layer];
_maskLayer.frame = self.bounds;
CGFloat angleStep = 2*M_PI/(_numSegments+1);
CGFloat startAngle = angleStep/2 + M_PI_2;
CGFloat endAngle = startAngle+_numSegments*angleStep+0.005; //add a bit that the outline is not clipped
_maskLayer.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)) radius:_circleRadius startAngle:startAngle endAngle:endAngle clockwise:YES].CGPath;
_maskLayer.fillColor = [UIColor clearColor].CGColor;
_maskLayer.strokeColor = [UIColor redColor].CGColor;
_maskLayer.lineWidth = 2*_circleWidth+2; //stroke is centered -> *2 to cover all
self.mask = _maskLayer;
}
- (void)setProgress:(CGFloat)progress{
_progress = MAX(0, MIN(1, progress));
_maskLayer.strokeEnd = _progress;
}
-(void)drawInContext:(CGContextRef)ctx{
// would get better gradient joints by not using antialias, but since they are hidden, it is not needed to adjust it
// CGContextSetAllowsAntialiasing(ctx, NO);
UIGraphicsPushContext(ctx);
//some values to adjust the circle
UIColor *startColor = [UIColor colorWithRed:1 green:0 blue:21/255. alpha:1];
UIColor *endColor = [UIColor colorWithRed:0 green:180/255. blue:35/255. alpha:1];
CGPoint centerPoint = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
//calculate startAngle and the increment between segements
CGFloat angleStep = 2*M_PI/(_numSegments+1);
CGFloat startAngle = angleStep/2 + M_PI_2;
//convert colors to hsv
CGFloat startHue,startSat,startBrightness,startAlpha;
CGFloat endHue,endSat,endBrightness,endAlpha;
[startColor getHue:&startHue saturation:&startSat brightness:&startBrightness alpha:&startAlpha];
[endColor getHue:&endHue saturation:&endSat brightness:&endBrightness alpha:&endAlpha];
if(endHue<startHue)
endHue+=1;
//draw the segments
for(int i=0;i<_numSegments;i++){
//calcualte segment startColor
CGFloat hue = startHue+((endHue-startHue)*i)/_numSegments;
if(hue>1)
hue-=1;
CGFloat brightness = startBrightness+((endBrightness-startBrightness)*i)/(_numSegments);
if(_numSegments==7){
//just increasing the brighness a bit to get more yellow like on your picture ;)
brightness = i>3?startBrightness+((endBrightness-startBrightness)*(i-4))/(_numSegments-4):startBrightness;
}
UIColor *fromColor = [UIColor colorWithHue:hue saturation:startSat+((endSat-startSat)*i)/_numSegments brightness:brightness alpha:startAlpha+((endAlpha-startAlpha)*i)/_numSegments];
//calculate segement endColor
hue = startHue+((endHue-startHue)*(i+1))/_numSegments;
if(hue>1)
hue-=1;
brightness = startBrightness+((endBrightness-startBrightness)*i)/(_numSegments);
if(_numSegments==7){
brightness = i>3?startBrightness+((endBrightness-startBrightness)*(i-3))/(_numSegments-4):startBrightness;
}
UIColor *toColor = [UIColor colorWithHue:hue saturation:startSat+((endSat-startSat)*(i+1))/_numSegments brightness:brightness alpha:startAlpha+((endAlpha-startAlpha)*(i+1))/_numSegments];
//actually draw the segment
[self drawSegmentAtCenter:centerPoint from:startAngle to:startAngle+angleStep radius:_circleRadius width:_circleWidth startColor:fromColor endColor:toColor];
startAngle+=angleStep;
}
//start clearing the inside
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeClear);
UIBezierPath* innerPath = [UIBezierPath bezierPath];
[innerPath moveToPoint:centerPoint];
[innerPath addArcWithCenter:centerPoint radius:_circleRadius-_circleWidth-0.5 startAngle:0 endAngle:2*M_PI clockwise:YES];
[innerPath fill];
}
- (void)drawSegmentAtCenter:(CGPoint)center from:(CGFloat)startAngle to:(CGFloat)endAngle radius:(CGFloat)radius width:(CGFloat)width startColor:(UIColor *)startColor endColor:(UIColor*)endColor{
CGContextSaveGState(UIGraphicsGetCurrentContext());
//apply a clip arc for the gradient
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:center];
[path addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
[path addClip];
//draw the gradient
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat locations[] = { 0.0, 1.0 };
NSArray *colors = @[(__bridge id) startColor.CGColor, (__bridge id) endColor.CGColor];
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef) colors, locations);
CGPoint startPoint = CGPointMake(center.x-sinf(startAngle-M_PI_2)*radius, center.y+cosf(startAngle-M_PI_2)*radius);
CGPoint endPoint = CGPointMake(center.x-sinf(endAngle-M_PI_2)*radius, center.y+cosf(endAngle-M_PI_2)*radius);
CGContextDrawLinearGradient(UIGraphicsGetCurrentContext(), gradient, startPoint, endPoint, kCGGradientDrawsAfterEndLocation|kCGGradientDrawsBeforeStartLocation);
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
CGContextRestoreGState(UIGraphicsGetCurrentContext());
}
@end
CircleOutlineLayer
@implementation CircleOutlineLayer{
int _numSegments;
CGFloat _circleradius;
CGFloat _circlewidth;
}
+(id)layer{
CircleOutlineLayer *layer = [[CircleOutlineLayer alloc] init];
return layer;
}
-(void)setCircleData:(NSDictionary*)data{
_numSegments = [data[@"numberOfSegments"] intValue];
_circleradius = [data[@"circleRadius"] doubleValue];
_circlewidth = [data[@"circleWidth"] doubleValue];
}
-(void)drawInContext:(CGContextRef)ctx{
UIGraphicsPushContext(ctx);
//some values to adjust the circle
[[UIColor colorWithWhite:130/255. alpha:1] setStroke]; //the outline color
CGPoint centerPoint = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
//calculate startAngle and the increment between segements
CGFloat angleStep = 2*M_PI/(_numSegments+1);
CGFloat startAngle = angleStep/2 + M_PI_2;
//draw the segments
for(int i=0;i<_numSegments;i++){
//actually draw the segment
[self drawSegmentAtCenter:centerPoint from:startAngle to:startAngle+angleStep radius:_circleradius width:_circlewidth doFill:NO];
startAngle+=angleStep;
}
//draw an inner outline
UIBezierPath *innerPath = [UIBezierPath bezierPath];
[innerPath moveToPoint:centerPoint];
[innerPath addArcWithCenter:centerPoint radius:_circleradius-_circlewidth startAngle:0 endAngle:2*M_PI clockwise:YES];
[innerPath stroke];
//start clearing the inside
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeClear);
innerPath = [UIBezierPath bezierPath];
[innerPath moveToPoint:centerPoint];
[innerPath addArcWithCenter:centerPoint radius:_circleradius-_circlewidth-0.5 startAngle:0 endAngle:2*M_PI clockwise:YES];
[innerPath fill];
//also clear a whole segment at the bottom to get rid of the inner outline
[self drawSegmentAtCenter:centerPoint from:-angleStep/2 + M_PI_2 to:angleStep/2+M_PI_2 radius:_circleradius width:_circlewidth doFill:YES];
//redraw the outlines at begin and end of circle since beginning was just cleared and end-outline hasn't been drawed
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeNormal);
CGPoint startPoint = CGPointMake(centerPoint.x+sinf(-angleStep/2)*(_circleradius), centerPoint.y+cosf(-angleStep/2)*(_circleradius));
CGPoint endPoint = CGPointMake(centerPoint.x+sinf(-angleStep/2)*(_circleradius-_circlewidth), centerPoint.y+cosf(-angleStep/2)*(_circleradius-_circlewidth));
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 1);
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), startPoint.x, startPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), endPoint.x,endPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
startPoint = CGPointMake(centerPoint.x+sinf(angleStep/2)*(_circleradius), centerPoint.y+cosf(angleStep/2)*(_circleradius));
endPoint = CGPointMake(centerPoint.x+sinf(angleStep/2)*(_circleradius-_circlewidth), centerPoint.y+cosf(angleStep/2)*(_circleradius-_circlewidth));
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), startPoint.x, startPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), endPoint.x, endPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
}
- (void)drawSegmentAtCenter:(CGPoint)center from:(CGFloat)startAngle to:(CGFloat)endAngle radius:(CGFloat)radius width:(CGFloat)width doFill:(BOOL)fill{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:center];
[path addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
if(fill)
[path fill];
[path stroke];
}
@end
Problem
Looking for a solution of my problem where I have to fill a color in ring. I am able to fill a color in ring with my code but there is some problem I am facing With this code I am not able to find How do I fill desire amount of color in circle i.e. 20%, 40%, 70% etc. When I give gradient color its not as shown in the image. Code I am using: ``` int radius = 130; CAShapeLayer *arc = [CAShapeLayer layer]; arc.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100,0) radius:radius startAngle:39.8 endAngle:19.9 clockwise:YES].CGPath; arc.position = CGPointMake(CGRectGetMidX(self.frame)-radius, CGRectGetMidY(self.frame)-radius); arc.fillColor = [UIColor clearColor].CGColor; arc.strokeColor = [UIColor redColor].CGColor; arc.lineWidth = 25; CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; drawAnimation.duration = 5.0; // "animate over 10 seconds or so.." drawAnimation.repeatCount = 1.0; // Animate only once.. drawAnimation.removedOnCompletion = NO; // Remain stroked after the animation.. drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f]; drawAnimation.toValue = [NSNumber numberWithFloat:100.0f]; drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; [arc addAnimation:drawAnimation forKey:@"drawCircleAnimation"]; CAGradientLayer *gradientLayer = [CAGradientLayer layer]; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); UIColor *gradientColor = [UIColor colorWithRed:0.51 green:0.0 blue:0.49 alpha:1.0]; NSArray *gradientColors = [NSArray arrayWithObjects: (id)[UIColor blueColor].CGColor, (id)gradientColor.CGColor, (id)[UIColor redColor].CGColor, nil]; CGFloat gradientLocations[] = {0, 0.5, 1}; CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)gradientColors, gradientLocations); gradientLayer.colors = (__bridge NSArray *)(gradient); gradientLayer.startPoint = CGPointMake(0.0,0.7); gradientLayer.endPoint = CGPointMake(1,-0.1); [self.layer addSublayer:gradientLayer]; gradientLayer.mask = arc; ``` Important: color must be as shown in image I am looking for very particular answer for this problem. Thanks In advance to all Who get a time to look into this.