Change CALayer color while animating

2.8k views Asked by At

APPROACH 1

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
 [CATransaction begin];
    {
        [CATransaction setAnimationDuration:15];//Dynamic Duration
        [CATransaction setCompletionBlock:^{

        }];

        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
        animation.autoreverses = NO;
        animation.removedOnCompletion = NO;
        animation.fromValue = @0;
        animation.toValue = @1;
        animation.timeOffset = 0;
        animation.fillMode = kCAFillModeForwards;
        [self.pathLayer addAnimation:animation forKey:animationKey];

    }
    [CATransaction commit];

I have added CAShapeLayer (pathLayer) in my view and I want it to animate around the view with stroke effect, the code above does the job but my problem is to change color in 3 equal proportions. So what I am assuming is to repeat the above code 3 times and change the following lines in respective order.

for 1st

    animation.fromValue = @0;
    animation.toValue = @(1/3);
    animation.timeOffset = 0;

for 2nd

    animation.fromValue = @(1/3);
    animation.toValue = @(2/3);
    animation.timeOffset = 0;// I don't know how to exactly set this  active local 
time since the duration which is currently 15 is dynamic can be 30 or 10.

for 3rd

    animation.fromValue = @(2/3);
    animation.toValue = @(3);
    animation.timeOffset = 0;// Active local time- Not sure how and which value to set 

APPROACH 2

Instead of 3 transactions with offset technique lets start 2nd transaction when 1st completes and 3rd when 2nd. But the fraction of time that is taken to start the new animation when one is completed a lag/jerk is visible.

APPROACH 3

SubClass CAShapeLayer

By doing SubClass, the drawInContext method is called only once, and if some extra property is added and it is changed the drawInContext method is called repeatedly and this way the layer color can be changed after specific progress period of time. But overriding the drawInContext method doesn't serve the purpose.

Any Suggestions ? I don't want to implement NSTimer separately.

1

There are 1 answers

8
ipmcc On BEST ANSWER

I'm not 100% clear on what you want here, but if the goal is just for the whole stroke to change color as it draws, but in three discrete stages, then I would propose adding the following. I cooked up these examples in a default "Single View Application" template. I've got a button set up with its action pointing at -doStuff:. If the whole stroke color were to change, it might look something like this:

Whole stroke changes color

To produce that, the code looked like:

@implementation MyViewController
{
    CAShapeLayer* mLayer;
}

- (IBAction)doStuff:(id)sender
{
    const NSUInteger numSegments = 3;
    const CFTimeInterval duration = 2;

    [mLayer removeFromSuperlayer];

    mLayer = [[CAShapeLayer alloc] init];
    mLayer.frame = CGRectInset(self.view.bounds, 100, 200);
    mLayer.fillColor = [[UIColor purpleColor] CGColor];
    mLayer.lineWidth = 12.0;
    mLayer.lineCap = kCALineCapSquare;
    mLayer.strokeEnd = 0.0;
    mLayer.path = [[UIBezierPath bezierPathWithRect: mLayer.bounds] CGPath]; // This can be whatever.

    [self.view.layer addSublayer: mLayer];

    [CATransaction begin];
    {
        [CATransaction setAnimationDuration: duration];// Dynamic Duration
        [CATransaction setCompletionBlock:^{ NSLog(@"Done"); }];

        const double portion = 1.0 / ((double)numSegments);
        NSMutableArray* values = [NSMutableArray arrayWithCapacity: numSegments];
        NSMutableArray* times = [NSMutableArray arrayWithCapacity: numSegments + 1];
        for (NSUInteger i = 0; i < numSegments; i++)
        {
            [values addObject: (__bridge id)[[UIColor colorWithHue: i * portion saturation:1 brightness:1 alpha:1] CGColor]];
            [times addObject: @(i * portion)];
        }

        [times addObject: @(1.0)]; // Have to add this, otherwise the last value wont get used.

        {
            CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath: @"strokeColor"];
            animation.keyTimes = times;
            animation.values = values;
            animation.calculationMode = kCAAnimationDiscrete;
            animation.removedOnCompletion = NO;
            animation.timeOffset = 0;
            animation.fillMode = kCAFillModeForwards;
            [mLayer addAnimation: animation forKey: @"strokeColor"];
        }
        {
            CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath: @"strokeEnd"];
            animation.fromValue = @(0);
            animation.toValue = @(1);
            animation.removedOnCompletion = NO;
            animation.timeOffset = 0;
            animation.fillMode = kCAFillModeForwards;
            [mLayer addAnimation: animation forKey: @"strokeEnd"];
        }
    }
    [CATransaction commit];
}

@end

Alternately, if the goal is to have three different segments of the stroke, all with different colors, that's a little more complicated, but can still be done with the same basic principals. One thing to note is that, without custom drawing, your CAShapeLayers can't have more than one stroke color (AFAIK), so you'll need to break this up into several sublayers.

This next example puts a shape layer into the view and then adds the sublayers for each part of the stroke and sets up the animation such that it appears theres a single, multi-color stroke being drawn, where each segment is a separate color. Here's roughly what it looked like:

Animated GIF of output

Here's the code:

@implementation MyViewController
{
    CAShapeLayer* mLayer;
}

- (IBAction)doStuff:(id)sender
{
    const NSUInteger numSegments = 3;
    const CFTimeInterval duration = 2;

    [mLayer removeFromSuperlayer];

    mLayer = [[CAShapeLayer alloc] init];
    mLayer.frame = CGRectInset(self.view.bounds, 100, 200);
    mLayer.fillColor = [[UIColor purpleColor] CGColor];
    mLayer.lineWidth = 12.0;
    mLayer.lineCap = kCALineCapSquare;
    mLayer.path = [[UIBezierPath bezierPathWithRect: mLayer.bounds] CGPath]; // This can be whatever.

    [self.view.layer addSublayer: mLayer];

    [CATransaction begin];
    {
        [CATransaction setAnimationDuration: duration];//Dynamic Duration
        [CATransaction setCompletionBlock:^{ NSLog(@"Done"); }];

        const double portion = 1.0 / ((double)numSegments);

        for (NSUInteger i = 0; i < numSegments; i++)
        {
            CAShapeLayer* strokePart = [[CAShapeLayer alloc] init];
            strokePart.fillColor = [[UIColor clearColor] CGColor];
            strokePart.frame = mLayer.bounds;
            strokePart.path = mLayer.path;
            strokePart.lineCap = mLayer.lineCap;
            strokePart.lineWidth = mLayer.lineWidth;

            // These could come from an array or whatever, this is just easy...
            strokePart.strokeColor = [[UIColor colorWithHue: i * portion saturation:1 brightness:1 alpha:1] CGColor];
            strokePart.strokeStart = i * portion;
            strokePart.strokeEnd = (i + 1) * portion;

            [mLayer addSublayer: strokePart];

            CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath: @"strokeEnd"];
            NSArray* times = @[ @(0.0), // Note: This works because both the times and the stroke start/end are on scales of 0..1
                                @(strokePart.strokeStart),
                                @(strokePart.strokeEnd),
                                @(1.0) ];
            NSArray* values = @[ @(strokePart.strokeStart),
                                 @(strokePart.strokeStart),
                                 @(strokePart.strokeEnd),
                                 @(strokePart.strokeEnd) ];

            animation.keyTimes = times;
            animation.values = values;
            animation.removedOnCompletion = NO;
            animation.fillMode = kCAFillModeForwards;
            [strokePart addAnimation: animation forKey: @"whatever"];
        }
    }
    [CATransaction commit];
}

@end

I'm not sure I've exactly understood what you were going for, but hopefully one of these is helpful.