Bernd Rabe
Bernd Rabe

Reputation: 790

Custom CAGradientLayer doesn't animate

I've made a custom class of CAGradientLayer (added radial gradient option). I works but I can't figure out why I can't animate the colors.

Here's my code.

The animation is a simple CABasicAnimation where I set the toValue to a new array with colors.

#import "ExtCAGradientLayer.h"

NSString * const kExt_CAGradientLayerRadial = @"RadialGradient";

@implementation ExtCAGradientLayer
@synthesize gradientDrawingOptions;

+(BOOL)needsDisplayForKey:(NSString *)key
{
   return [key isEqualToString:@"type"] || [super needsDisplayForKey:key];
}

- (void)drawInContext:(CGContextRef)theContext
{
   if ([self.type isEqualToString:kExt_CAGradientLayerRadial]) {
    size_t num_locations = self.locations.count;

    int numbOfComponents = 0;
    CGColorSpaceRef colorSpace = NULL;

    if (self.colors.count) {
        CGColorRef colorRef = (__bridge CGColorRef)[self.colors objectAtIndex:0];
        numbOfComponents = CGColorGetNumberOfComponents(colorRef);
        colorSpace = CGColorGetColorSpace(colorRef);
    }

    float *locations = calloc(num_locations, sizeof(float));
    float *components = calloc(num_locations, numbOfComponents * sizeof(float));

    for (int x = 0; x < num_locations; x++) {
        locations[x] = [[self.locations objectAtIndex:x] floatValue];
        const CGFloat *comps = CGColorGetComponents((__bridge CGColorRef)[self.colors objectAtIndex:x]);
        for (int y = 0; y < numbOfComponents; y++) {
            int shift = numbOfComponents * x;
            components[shift + y] = comps[y];
        }
    }

    CGPoint position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
    CGFloat radius = floorf(MIN(self.bounds.size.width, self.bounds.size.height) / 2);
    CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, components, locations, num_locations);

    CGContextDrawRadialGradient(theContext, gradient, position, 0, position, radius, self.gradientDrawingOptions);

    free(locations);
    free(components);
}

Upvotes: 1

Views: 2102

Answers (1)

Rob Napier
Rob Napier

Reputation: 299623

Layers have a bunch of implicit animations tied to them. This can make them a little funny when interacting with CABasicAnimation. My first recommendation is to avoid CABasicAnimation when working with layers. There's almost never a good reason for it, since layers know how to animate themselves. It's much easier just to do this:

[CATransaction begin];
[CATransaction setAnimationDuration:3];
layer.colors = [NSArray arrayWithObjects:
              (__bridge id)[UIColor redColor].CGColor,
              [UIColor greenColor].CGColor,
              nil]; 
[CATransaction commit];

That's a 3-second animation of the colors. Easy. The begin/commit aren't technically needed in most cases, but they limit the scope of the setAnimationDuration: in case you're animating other things in this transaction.

But maybe you like CABasicAnimation, or you're using it something that returns them (I do that sometimes). You can still combine them with CALayer, but you have to tell CALayer to stop its automatic animation (setDisableActions:). And you still want to use the layer setters rather than toValue:.

[CATransaction begin];
[CATransaction setDisableActions:YES];
CABasicAnimation *anim = [[CABasicAnimation alloc] init];
anim.duration = 3;
[layer addAnimation:anim forKey:@"colors"];
layer.colors = [NSArray arrayWithObjects:
                (__bridge id)[UIColor redColor].CGColor,
                [UIColor greenColor].CGColor,
                nil];
[CATransaction commit];

If you use toValue: rather than the setter, it will work, but will "jump back" to the original value at the end.

Avoid solutions that suggest setting fillMode and removedOnCompletion. These generate many subtle problems if you don't understand exactly what they're doing. They seldom are what you really mean; they just happen to work as long as things are very simple.

Upvotes: 4

Related Questions