Maysam
Maysam

Reputation: 7367

Creating animated layer stroke

I want to create something like this, just consider single loop and how it completes a circle and reverse it on completion:

enter image description here

This piece of code does half of what I want:

CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    drawAnimation.duration            = 1;
    drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
    drawAnimation.toValue   = [NSNumber numberWithFloat:1.0f];

    drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

    [circleLayer addAnimation:drawAnimation forKey:@"drawCircleAnimation"];

I tried it to reverse but does not work:

[CATransaction begin];
    CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
    drawAnimation.duration            = 1;
    drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
    drawAnimation.toValue   = [NSNumber numberWithFloat:1.0f];

    drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

    //[circleLayer addAnimation:drawAnimation forKey:@"drawCircleAnimation"];
    [CATransaction setCompletionBlock:^{
        CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
        animation2.duration = 1;
        animation2.fromValue = [NSNumber numberWithFloat:0.0f];
        animation2.toValue = [NSNumber numberWithFloat:1.0f];
        animation2.removedOnCompletion = NO;
        animation2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
        [circleLayer addAnimation:animation2 forKey:@"circelBack"];

    }];

    [circleLayer addAnimation:drawAnimation forKey:@"circleFront"];

    [CATransaction commit];

The problem is I cannot reverse the animation.

Upvotes: 1

Views: 288

Answers (1)

Hamish
Hamish

Reputation: 80781

The Problem

First of all, I suspect you've got the key paths of your animations round the wrong way. You should first be animating the stroke end from 0 to 1, and then the stroke start from 0 to 1.

Secondly, you're never updating the model layer with your new values - so when the animation is complete, the layer will 'snap back' to its original state. For you, this means that when the first animation is done - the strokeStart will snap back to 0.0 - therefore the reverse animation will look weird.


The Solution

To update the model layer values, you can simply set disableActions to YES in your CATransaction block to prevent an implicit animations from being generated on layer property changes (won't affect explicit animations). You'll then want to update the model layer's property after you add the animation to the layer.

Also, you can re-use CAAnimations – as they are copied when added to a layer. Therefore you can define the same animation for both the forward and reverse animation, just changing the key path.

If you're repeating the animation, you'll probably want to define your animation as an ivar - and simply update the key path before adding it.

For example, in your viewDidLoad:

@implementation ViewController {
    CABasicAnimation* drawAnimation;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    // define your animation
    drawAnimation = [CABasicAnimation animation];
    drawAnimation.duration = 1;

    // use an NSNumber literal to make your code easier to read
    drawAnimation.fromValue = @(0.0f);
    drawAnimation.toValue   = @(1.0f);

    // your timing function
    drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

    // kick off the animation loop
    [self animate];
}

You'll then want to create an animate method in order to perform one iteration of the animation. Unfortunately, since Core Animation doesn't support repeating multiple animations in a sequence, you'll have to use recursion to achieve your effect.

For example:

-(void) animate {

    if (self.circleLayer.superlayer) { // check circle layer is the layer heirachy before attempting to animate

        // begin your transaction
        [CATransaction begin];

        // prevent implicit animations from being generated
        [CATransaction setDisableActions:YES];

        // reset values
        self.circleLayer.strokeEnd = 0.0;
        self.circleLayer.strokeStart = 0.0;

        // update key path of animation
        drawAnimation.keyPath = @"strokeEnd";

        // set your completion block of forward animation
        [CATransaction setCompletionBlock:^{

            // weak link to self to prevent a retain cycle
            __weak typeof(self) weakSelf = self;

            // begin new transaction
            [CATransaction begin];

            // prevent implicit animations from being generated
            [CATransaction setDisableActions:YES];

            // set completion block of backward animation to call animate (recursive)
            [CATransaction setCompletionBlock:^{
                [weakSelf animate];
            }];

            // re-use your drawAnimation, just changing the key path
            drawAnimation.keyPath = @"strokeStart";

            // add backward animation
            [weakSelf.circleLayer addAnimation:drawAnimation forKey:@"circleBack"];

            // update your layer to new stroke start value
            weakSelf.circleLayer.strokeStart = 1.0;

            // end transaction
            [CATransaction commit];

        }];

        // add forward animation
        [self.circleLayer addAnimation:drawAnimation forKey:@"circleFront"];

        // update layer to new stroke end value
        self.circleLayer.strokeEnd = 1.0;

        [CATransaction commit];
    }
}

To stop the animation, you can remove the layer from the superlayer - or implement your own boolean check for whether the animation should continue.

enter image description here


Full Project: https://github.com/hamishknight/Circle-Pie-Animation

Upvotes: 1

Related Questions