Aliaksandr B.
Aliaksandr B.

Reputation: 146

Animated PieChart with gradient in arcs

I'm trying to animate drawing sections in my PieChart view. So, every sector has it's own colour, and sector grows from some start to end angles.

For that, I've used

-(id<CAAction>)actionForKey:(NSString *)event {
    if ([event isEqualToString:@"startAngle"] ||
        [event isEqualToString:@"endAngle"]) {
        return [self makeAnimationForKey:event];
    }

    return [super actionForKey:event];
}

-(CABasicAnimation *)makeAnimationForKey:(NSString *)key {
   CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:key];
   // set up animation
   return animation;
}

So, these methods are working pretty well, but the problem is in using CGContextDrawLinearGradient instead of CGContextSetStrokeColorWithColor for colour segment. With CGContextSetStrokeColorWithColor segment drawing step by step, while I'm increasing end agle, but with CGContextDrawLinearGradient it's missing all intermediate values and drawing only final segment size. So, for user there is no any kid of animation only drawing segment at once, there is no any dynamic.

-(void)drawInContext:(CGContextRef)ctx {

   CGPoint center = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
   CGFloat radius = MIN(center.x, center.y);

   CGContextBeginPath(ctx);
   CGContextMoveToPoint(ctx, center.x, center.y);

   CGPoint p1 = CGPointMake(center.x + radius * cosf(self.startAngle), center.y + radius * sinf(self.startAngle));
   CGPoint p2 = CGPointMake(center.x + radius * cosf(self.endAngle), center.y + radius * sinf(self.endAngle));
   CGContextAddLineToPoint(ctx, p1.x, p1.y);

   int clockwise = self.startAngle > self.endAngle;

   CGContextAddArc(ctx, center.x, center.y, radius, self.startAngle, self.endAngle, clockwise);

   CGContextSaveGState(ctx);

   CGContextClip(ctx);

   CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
   CGGradientRef gradient = CGGradientCreateWithColors(space, (CFArrayRef)self.gradients, NULL);
    CGContextDrawLinearGradient(ctx, gradient, p1, p2, kCGGradientDrawsBeforeStartLocation);

   //    CGContextSetFillColorWithColor working properly
   //    CGContextSetFillColorWithColor(ctx, self.fillColor.CGColor);
   CGContextSetStrokeColorWithColor(ctx, self.strokeColor.CGColor);
   CGContextSetLineWidth(ctx, self.strokeWidth);
   CGContextDrawPath(ctx, kCGPathFillStroke);
}

My result:

Any comments will be appreciated, because I'm newbie with CoreGraphics.

Thank you!

Upvotes: 1

Views: 1510

Answers (2)

Matt Long
Matt Long

Reputation: 24476

I took a stab at this using a composited Core Animation Layer. It inherits from CAGradientLayer and then uses a CAShapeLayer to take advantage of the strokeStart and strokeEnd properties. The shape layer is used to mask off the gradient layer which allows only shape layer alpha to show through--which in this case is whatever the strokeStart and strokeEnd properties are set to. This allows you to dynamically set the pie sizes. Here is the visual output of what I came up with:

Pieces of Pie

The reason I used a gradient layer as the base class is because you were showing a gradient in your drawInContext: code. You could use a regular CALayer if you preferred.

Here is how I define the inherited layer class's init method:

- (instancetype)initWithRect:(CGRect)rect
{
    self = [super init];
    if (self) {
        // Give it some default gradient colors
        self.colors = @[(id)[[UIColor darkGrayColor] CGColor],
                        (id)[[UIColor lightGrayColor] CGColor]];

        // Linear gradient from left side to right side
        self.startPoint = CGPointMake(0.0f, 0.5f);
        self.endPoint = CGPointMake(1.0f, 0.5f);

        // Set our bounds based on what was passed in
        self.bounds = rect;

        // Initialize our shape layer and set its bounds and position
        _shapeLayer = [CAShapeLayer layer];
        _shapeLayer.bounds = rect;
        _shapeLayer.position = CGPointMake(self.bounds.size.width/2.0f, 
                                           self.bounds.size.height/2.0f);

        // Create an inner rect we'll use for the bezier path rectangle
        CGRect innerRect = CGRectInset(rect, self.bounds.size.width/4.0f,
                                       self.bounds.size.height/4.0f);

        UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:innerRect 
                                           cornerRadius:self.bounds.size.width/2.0f];

        // Set the path
        _shapeLayer.path = [bezierPath CGPath];

        // No fill color
        _shapeLayer.fillColor = [[UIColor clearColor] CGColor];

        // This color doesn't matter since we only care about whether or
        // not there is alpha when masking.
        _shapeLayer.strokeColor = [[UIColor redColor] CGColor];

        // Set the line with to half of the size since the stroke
        // is centered
        _shapeLayer.lineWidth = self.bounds.size.width/2.0f;

        // Set the mask
        self.mask = _shapeLayer;
    }
    return self;
}

Now you can declare each of your pie pieces like this:

MLGradientPieLayer *pie = [[MLGradientPieLayer alloc] initWithRect:kRect];
pie.colors = @[(id)[[UIColor orangeColor] CGColor], (id)[[UIColor yellowColor] CGColor]];
pie.position = kPosition;
[pie setStart:0.0f];
[pie setEnd:0.15f];

[self.view.layer addSublayer:pie];

You can see a full project on Github here: https://github.com/perlmunger/GradientPieLayer.git

I didn't add animations, however, you can animate the strokeStart and strokeEnd properties of the inner CAShapeLayer called (unimaginatively) _shapeLayer.

Upvotes: 1

David R&#246;nnqvist
David R&#246;nnqvist

Reputation: 56635

If you need to do custom animations have a look at:

Together they teach you a lot of good ways to do custom animations.

Upvotes: 0

Related Questions