0
votes

I'm working on a circular progress bar that I want to animate. Everything draws as expected and works but when I attempt to animate I get an immediate change instead of the animation. Any help would be appreciated.

Here is my layer:

@interface CircularProgressLayer : CALayer

@property (nonatomic, assign) CGFloat progressDegrees;
@property (nonatomic, assign) CGFloat trackWidth;

@end

@implementation CircularProgressLayer : CALayer

@dynamic progressDegrees;
@dynamic trackWidth;

- (id)initWithLayer:(id)layer
{
    if ((self = [super initWithLayer:layer]))
    {
        if ([layer isKindOfClass:[CircularProgressLayer class]])
        {
            self.progressDegrees = ((CircularProgressLayer*)layer).progressDegrees;
        }
    }

    return self;
}

// Instruct to Core Animation that a change in the custom property should automatically trigger a redraw of the layer.
+ (BOOL)needsDisplayForKey:(NSString*)key
{
    if ([key isEqualToString:@"progressDegrees"])
    {
        return YES;
    }
    if ([key isEqualToString:@"trackWidth"])
    {
        return YES;
    }

    return [super needsDisplayForKey:key];
}

// Needed to support implicit animation of this property.
// Return the basic animation the implicit animation will leverage.
- (id<CAAction>)actionForKey:(NSString *)key
{
    if ([key isEqualToString:@"progressDegrees"])
    {
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:key];
        animation.fromValue = [[self presentationLayer] valueForKey:key];
        animation.duration = 5.0;
        animation.fillMode = kCAFillModeForwards;
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];

        return animation;
    }
    return [super actionForKey:key];
}

As I understand it, returning the animation in the actionForKey: should be all that is needed to make the animation work, but I get nothing. All of my drawing is currently contained in the drawLayer:inContext: method of my view that implements this layer (cause it originated as drawRect code before I learned I had to put it in a layer to get it to animate, and I haven't converted everything over), but the sample code I downloaded from Apple makes it look like this is just fine.

Here is the drawing code:

-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
    UIGraphicsPushContext(context);
    [self drawTrackInContext:context];
    [self drawProgressInContext:context];
    UIGraphicsPopContext();
}

- (void)drawProgressInContext:(CGContextRef)context
{    
    if (self.progress < 1)
    {
        // No progress, get out
        return;
    }

    CGSize size = self.bounds.size;

    FlipContextVertically(size);
    CGContextSaveGState(context);
    {
        UIBezierPath *circle = [self circle];
        CGContextSetLineWidth(context, 0.0);

        CGFloat degrees = 360.0;

        for (int i = 0; i < degrees; i++)
        {
            if (i > ((RMONCircularProgressLayer *)self.layer).progressDegrees)
            {
                break;
            }

            CGFloat theta = 2 * M_PI * (CGFloat) i / degrees;
            CGFloat x = self.radius * sin(theta);
            CGFloat y = self.radius * cos(theta);

            MovePathCenterToPoint(circle, CGPointMake(x, y));
            OffsetPath(circle, CGSizeMake(size.width / 2, size.height / 2));

            CGContextSetFillColorWithColor(context, [self colorForDegree:i].CGColor);
            CGContextAddPath(context, circle.CGPath);
            CGContextDrawPath(context, kCGPathFillStroke);
        }
    }
    CGContextRestoreGState(context);
}

- (void)drawTrackInContext:(CGContextRef)context
{
    CGContextSaveGState(context);
    {
        // Draw the circle covering middle of the screen
        CGSize size = self.bounds.size;

        CGContextSetLineWidth(context, self.trackWidth);  // will only be filled color

        CGContextSetStrokeColorWithColor(context, self.trackColor.CGColor);
        CGContextSetFillColorWithColor(context, self.fillColor.CGColor);
        CGContextSetAlpha(context, 1.0);

        CGContextAddArc(context, (CGFloat)size.width/2.0, (CGFloat)size.height/2.0, self.radius, 0, (CGFloat)M_PI*2.0, 1);
        CGContextDrawPath(context, kCGPathFillStroke);
    }
    CGContextRestoreGState(context);
}

drawTrackInContext just draws a circle, where the progress circle will be contained. drawProgressInContext is the drawing code I'm expecting to animate. There are some helper functions from iOS Drawing by Erica Sadun that do some context and path manipulation that I didn't include, the function names should be fairly self explanatory.

Any help would be greatly appreciated.

FYI: I'm more than willing to make this an explicit animation if that's easier, but I ended up going in this direction because I couldn't make that work either.

1
Please show your code inside drawLayer:inContext:Eric Qian
Added the drawing code, don't know why I forgot it.Rob Booth

1 Answers

3
votes

When a layer property is animated, the value of that property does not change over time to represent the intermediate values. Instead, the values of the property for that given moment in layer time is derived from the active animations and made available in the layer's presentationLayer.

When an animation is active, drawInContext: is called on the presentation layer rather than the main layer. Similarly, -drawLayer:inContext: receives the presentation layer rather than the main layer.

The problem you're experiencing is that -drawProgressInContext: doesn't use the layer from the calling function and instead reads self.layer.progressDegrees to get the value to draw, which will always be the assigned value. The fix is to pass the layer to that function and read the value:

- (void)drawProgressInLayer:(CALayer *)layer context:(CGContextRef)context
{
    ...
    if (i > ((RMONCircularProgressLayer *)layer).progressDegrees) {
        ...

You also have an issue when you abort on if (self.progress < 1). That will make it fail to animate when going from a non-zero value to zero. The same fix would apply: if ((RMONCircularProgressLayer *)layer).progressDegrees < 1)