Search code examples
ioscore-animationcore-graphics

Creating animated containing circles with Core Graphics and Core Animations


I'm trying some time, but without success. I made some circle with animation that I want. It animates from radius 23 to radius 7.

Shema

There is a code that's working fine, but without transparent circle in it.

I need help to get this "transparent inner circle" working during animation.

Some CustomLayer:

@dynamic circleRadius; // Linked post tells us to let CA implement our accessors for us.
// Whether this is necessary or not is unclear to me and one 
// commenter on the linked post claims success only when using
// @synthesize for the animatable property.



+ (BOOL)needsDisplayForKey:(NSString*)key {
    // Let our layer know it has to redraw when circleRadius is changed
    if ([key isEqualToString:@"circleRadius"]) {
        return YES;
    } else {
        return [super needsDisplayForKey:key];
    }
}

- (void)drawInContext:(CGContextRef)ctx {

    // This call is probably unnecessary as super's implementation does nothing
    [super drawInContext:ctx];


    CGRect rect = CGContextGetClipBoundingBox(ctx);

    CGContextSetRGBFillColor(ctx, 1.000, 0.533, 0.000, 0.1);
    CGContextSetRGBStrokeColor(ctx, 1.000, 0.533, 0.000, 0.5);

    // Construct a CGMutablePath to draw the light blue circle
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddArc(path, NULL, rect.size.width / 2, 
                 rect.size.height / 2, 
                 self.circleRadius, 0, 2 * M_PI, NO);


    // Fill the circle
    CGContextAddPath(ctx, path);

    CGContextFillPath(ctx);

    // Stroke the circle's border
    CGContextAddPath(ctx, path);
    CGContextStrokePath(ctx);

    // Release the path
    CGPathRelease(path);

    CGContextStrokePath(ctx);

}

and animation part somewhere in mu UIView

CustomLayer *customLayer = [[CustomLayer alloc] init];

    if ([customLayer respondsToSelector:@selector(setContentsScale:)])
    {
        [customLayer setContentsScale:[[UIScreen mainScreen] scale]];
    }

    // Make layer big enough for the initial radius
    // EDIT: You may want to shrink the layer when it reacehes it's final size
    [customLayer setFrame:CGRectMake(0, 0, 57, 52)];
    [self.layer addSublayer:customLayer];


    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"circleRadius"];
    animation.repeatCount = MAXFLOAT;
    // Zoom in, oscillate a couple times, zoom in further
    animation.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:23], 
                        [NSNumber numberWithFloat:22], 
                        [NSNumber numberWithFloat:20], 
                        [NSNumber numberWithFloat:18], 
                        [NSNumber numberWithFloat:15], 
                        [NSNumber numberWithFloat:13],
                        [NSNumber numberWithFloat:11],
                        [NSNumber numberWithFloat:9],
                        [NSNumber numberWithFloat:7], 
                        [NSNumber numberWithFloat:7],
                        [NSNumber numberWithFloat:7],
                        [NSNumber numberWithFloat:7],
                        nil];
    // We want the radii to be 20 in the end
    customLayer.circleRadius = 7;

    // Rather arbitrary values.  I thought the cubic pacing w/ a 2.5 second pacing
    // looked decent enough but you'd probably want to play with them to get a more
    // accurate imitation of the Maps app.  You could also define a keyTimes array for 
    // a more discrete control of the times per step.
    animation.duration = 1.5;
    animation.calculationMode = kCAAnimationCubicPaced;

    [customLayer addAnimation:animation forKey:nil];

Solution

  • For the case where you want a "donut", you are going to need to build up the drawing something like this:

    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddArc(path, NULL, rect.size.width / 2, 
                 rect.size.height / 2, 
                 self.circleRadius, 0, 2 * M_PI, NO);
    CGPathAddArc(path, NULL, rect.size.width / 2, 
                 rect.size.height / 2, 
                 self.circleRadius/2.0, 0, 2 * M_PI, NO);
    CGContextAddPath(ctx, path);
    
    CGContextSetRGBFillColor(ctx, 1.000, 0.533, 0.000, 0.1);
    CGContextEOFillPath(ctx);  // Note - you want the even-odd fill rule to get the donut
    
    CGPathRelease( path);
    
    // now the outside stroke
    
    CGContextBeginPath( ctx ); // removes previous path
    path = CGPathCreateMutable();
    CGPathAddArc(path, NULL, rect.size.width / 2, 
             rect.size.height / 2, 
             self.circleRadius, 0, 2 * M_PI, NO);
    
    CGContextSetRGBStrokeColor(ctx, 1.000, 0.533, 0.000, 0.5);
    
    CGContextAddPath(ctx, path);
    CGContextStrokePath(ctx);
    
    CGPathRelease( path)
    

    For the case where you just want to stroke the outer path, do just that. Do not do a path fill.