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.

Original source