How to chain different CAAnimation in an iOS application
Asked Answered
R

5

23

I need to chain animations, CABasicAnimation or CAAnimationGroup but I don't know how to do it, the only that I do is that all the animation execute at the same time for the same layer.

How could I do it?

For example, a layer with its contents set to a car image:

1st: move X points to right

2nd: Rotate 90ª to left

3rd: Move X point

4th: Scale the layer

All this animations must be executed in a secuencial way, but I can't do it :S

BTW: I am not english, sorry if I made some mistakes in my grammar :D

Ragin answered 31/7, 2012 at 10:3 Comment(2)
Do you intend to do more advanced animations as well or only moving, scaling and rotating (not 3D)?Choppy
Yes, I also use CATransform3DMakeRotation and othersRagin
C
23

tl;dr: You need to manually add each animation after the previous finishes.


There is no built in way to add sequential animations. You could set the delay of each animation to be the sum of all previous animations but I wouldn't recommend it.

Instead I would create all the animations and add them to a mutable array (using the array as a queue) in the order they are supposed to run. Then by setting yourself as the animations delegate to all the animations you can get the animationDidStop:finished: callback whenever an animation finishes.

In that method you will remove the first animation (meaning the next animation) from the array and add it to the layer. Since you are the delegate you will get a second animation when that one finishes in which case the animationDidStop:finished: callback will run again and the next animation is removed from the mutable array and added to the layer.

Once the array of animations is empty, all animations will have run.


Some sample code to get you started. First you set up all your animations:

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
[animation setToValue:(id)[[UIColor redColor] CGColor]];
[animation setDuration:1.5];
[animation setDelegate:self];
[animation setValue:[view layer] forKey:@"layerToApplyAnimationTo"];

// Configure other animations the same way ...

[self setSequenceOfAnimations:[NSMutableArray arrayWithArray: @[ animation, animation1, animation2, animation3, animation4, animation5 ] ]];

// Start the chain of animations by adding the "next" (the first) animation
[self applyNextAnimation];

Then in the delegate callback you simply apply the next animation again

- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished {
    [self applyNextAnimation];
}

- (void)applyNextAnimation {
     // Finish when there are no more animations to run
    if ([[self sequenceOfAnimations] count] == 0) return;

    // Get the next animation and remove it from the "queue"
    CAPropertyAnimation * nextAnimation = [[self sequenceOfAnimations] objectAtIndex:0];
    [[self sequenceOfAnimations] removeObjectAtIndex:0];

    // Get the layer and apply the animation
    CALayer *layerToAnimate = [nextAnimation valueForKey:@"layerToApplyAnimationTo"];
    [layerToAnimate addAnimation:nextAnimation forKey:nil];
}

I'm using a custom key layerToApplyAnimationTo so that each animation knows its layer (it works just by setValue:forKey: and valueForKey:).

Cliff answered 31/7, 2012 at 11:9 Comment(13)
David, how come you don't like setting different beginTime values for each animation? That's often simpler to code than chaining the animations using the completion method, and it works perfectly in my experience.Lentil
@DuncanC It feels to fragile to me. Adding, removing or changing the order of the animations become more cumbersome and prone to errors.Choppy
Thank you so much, I think both answers are valid to me, but the David aproach looks better to me.Ragin
I like David's approach, but it loses the ability to play your animation backwards.Morrissette
@RhythmicFistman You could read from the queue in the reverse order to play the sequence in reverse. If you also want the individual animations go backwards then you can multiply the speed of the individual animations by -1 go play them backwards. nextAnimation.speed *= -1;Choppy
@RhythmicFistman So maybe not "it loses the ability to" but fair enough. I think it's good that Duncan's and my solution both work while being very different. It shows that people have different preferences. FWIW Duncan's solution was a lot nicer than some other things I've seen been used and I upvoted it.Choppy
This is great. But we might be the delegate for many "sequenced" CAAnimations in this same fashion. Is there an easy way to tag the CAAnimation object so we can pick a logic path based on the sender?Gilbart
@RandyJames You can pass a key when adding the animation and use that key to look up the correct array to pick the next animation from. The key in addAnimation:forKey: is not the same as the key path in animationForKeyPath:Choppy
I see: [someView.layer addAnimation:anim forKey:@"someKey"]; But I don't see how to dereference that valie on the CAAnimation object received in the animationDidStop method.Gilbart
I have tried the "animation.beginTime = CACurrentMediaTime() + duration;" approach and DavidRönnqvists solution...And the one from DavidRönnqvist works perfect and it feels indeed less "fragile".Steinway
Where did setSequenceOfAnimations come from? What do I do with that?Whittington
@Whittington that's pre-dot-syntax for a property. It's simply the setter of a mutable array property that represents the remaining animations in the sequence. You can also see that applyNextAnimation reads and removes animations from the same property (using the pre-dot-syntax for the getter)Choppy
Ok.. Would you be kind and have a look at this question of mine and share your thoughts? This answer of yours is 3 years old and I'm not sure if this still applies to what I should be doing now. https://mcmap.net/q/584310/-how-to-properly-animate-sequentially-using-caanimation/5184188Whittington
L
36

What david suggests works fine, but I would recommend a different way.

If all your animations are to the same layer, you can create an animation group, and make each animation have a different beginTime, where the first animation starts at beginTime 0, and each new animation starts after the total duration of the animations before.

If your animations are on different layers, though, you can't use animation groups (all the animations in an animation group must act on the same layer.) In that case, you need to submit separate animations, each of which has a beginTime that is offset from CACurrentMediaTime(), e.g.:

CGFloat totalDuration = 0;
CABasicAnimation *animationOne = [CABasicAnimation animationWithKeyPath: @"alpha"];
animationOne.beginTime = CACurrentMediaTime(); //Start instantly.
animationOne.duration = animationOneDuration;
...
//add animation to layer

totalDuration += animationOneDuration;

CABasicAnimation *animationTwo = [CABasicAnimation animationWithKeyPath: @"position"];
animationTwo.beginTime = CACurrentMediaTime() + totalDuration; //Start after animation one.
animationTwo.duration = animationTwoDuration;
...
//add animation to layer

totalDuration += animationTwoDuration;


CABasicAnimation *animationThree = [CABasicAnimation animationWithKeyPath: @"position"];
animationThree.beginTime = CACurrentMediaTime() + totalDuration; //Start after animation three.
animationThree.duration = animationThreeDuration;
...
//add animation to layer

totalDuration += animationThreeDuration;
Lentil answered 31/7, 2012 at 20:42 Comment(1)
In case this helps someone else: if you DO use an animation group (ie. not the code snippet here but David's first paragraph), then animation.beginTim is relative to the group.... so you don't set it to CACurrentMediaTime(), you just set it to totalDuration. This had me puzzled for some minutes....Airworthy
C
23

tl;dr: You need to manually add each animation after the previous finishes.


There is no built in way to add sequential animations. You could set the delay of each animation to be the sum of all previous animations but I wouldn't recommend it.

Instead I would create all the animations and add them to a mutable array (using the array as a queue) in the order they are supposed to run. Then by setting yourself as the animations delegate to all the animations you can get the animationDidStop:finished: callback whenever an animation finishes.

In that method you will remove the first animation (meaning the next animation) from the array and add it to the layer. Since you are the delegate you will get a second animation when that one finishes in which case the animationDidStop:finished: callback will run again and the next animation is removed from the mutable array and added to the layer.

Once the array of animations is empty, all animations will have run.


Some sample code to get you started. First you set up all your animations:

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
[animation setToValue:(id)[[UIColor redColor] CGColor]];
[animation setDuration:1.5];
[animation setDelegate:self];
[animation setValue:[view layer] forKey:@"layerToApplyAnimationTo"];

// Configure other animations the same way ...

[self setSequenceOfAnimations:[NSMutableArray arrayWithArray: @[ animation, animation1, animation2, animation3, animation4, animation5 ] ]];

// Start the chain of animations by adding the "next" (the first) animation
[self applyNextAnimation];

Then in the delegate callback you simply apply the next animation again

- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished {
    [self applyNextAnimation];
}

- (void)applyNextAnimation {
     // Finish when there are no more animations to run
    if ([[self sequenceOfAnimations] count] == 0) return;

    // Get the next animation and remove it from the "queue"
    CAPropertyAnimation * nextAnimation = [[self sequenceOfAnimations] objectAtIndex:0];
    [[self sequenceOfAnimations] removeObjectAtIndex:0];

    // Get the layer and apply the animation
    CALayer *layerToAnimate = [nextAnimation valueForKey:@"layerToApplyAnimationTo"];
    [layerToAnimate addAnimation:nextAnimation forKey:nil];
}

I'm using a custom key layerToApplyAnimationTo so that each animation knows its layer (it works just by setValue:forKey: and valueForKey:).

Cliff answered 31/7, 2012 at 11:9 Comment(13)
David, how come you don't like setting different beginTime values for each animation? That's often simpler to code than chaining the animations using the completion method, and it works perfectly in my experience.Lentil
@DuncanC It feels to fragile to me. Adding, removing or changing the order of the animations become more cumbersome and prone to errors.Choppy
Thank you so much, I think both answers are valid to me, but the David aproach looks better to me.Ragin
I like David's approach, but it loses the ability to play your animation backwards.Morrissette
@RhythmicFistman You could read from the queue in the reverse order to play the sequence in reverse. If you also want the individual animations go backwards then you can multiply the speed of the individual animations by -1 go play them backwards. nextAnimation.speed *= -1;Choppy
@RhythmicFistman So maybe not "it loses the ability to" but fair enough. I think it's good that Duncan's and my solution both work while being very different. It shows that people have different preferences. FWIW Duncan's solution was a lot nicer than some other things I've seen been used and I upvoted it.Choppy
This is great. But we might be the delegate for many "sequenced" CAAnimations in this same fashion. Is there an easy way to tag the CAAnimation object so we can pick a logic path based on the sender?Gilbart
@RandyJames You can pass a key when adding the animation and use that key to look up the correct array to pick the next animation from. The key in addAnimation:forKey: is not the same as the key path in animationForKeyPath:Choppy
I see: [someView.layer addAnimation:anim forKey:@"someKey"]; But I don't see how to dereference that valie on the CAAnimation object received in the animationDidStop method.Gilbart
I have tried the "animation.beginTime = CACurrentMediaTime() + duration;" approach and DavidRönnqvists solution...And the one from DavidRönnqvist works perfect and it feels indeed less "fragile".Steinway
Where did setSequenceOfAnimations come from? What do I do with that?Whittington
@Whittington that's pre-dot-syntax for a property. It's simply the setter of a mutable array property that represents the remaining animations in the sequence. You can also see that applyNextAnimation reads and removes animations from the same property (using the pre-dot-syntax for the getter)Choppy
Ok.. Would you be kind and have a look at this question of mine and share your thoughts? This answer of yours is 3 years old and I'm not sure if this still applies to what I should be doing now. https://mcmap.net/q/584310/-how-to-properly-animate-sequentially-using-caanimation/5184188Whittington
H
8

Here's a solution in Swift:

var animations = [CABasicAnimation]()

var animation1 = CABasicAnimation(keyPath: "key_path_1")
// animation set up here, I've included a few properties as an example
animation1.duration = 1.0
animation1.fromValue = 1
animation1.toValue = 0
animations.append(animation1)
    
var animation2 = CABasicAnimation(keyPath: "key_path_2")
animation2.duration = 1.0
animation2.fromValue = 1
animation2.toValue = 0
// setting beginTime is the key to chaining these
animation2.beginTime = 1.0
animations.append(animation2)
    
let group = CAAnimationGroup()
group.duration = 2.0
group.repeatCount = FLT_MAX
group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
group.animations = animations
    
yourLayer.addAnimation(group, forKey: nil)
Helen answered 22/3, 2016 at 23:4 Comment(0)
W
1

Either David's approach or the beginTime property are ok for chaining animations. See http://wangling.me/2011/06/time-warp-in-animation.html - it clarifies the use of beginTime and other CAMediaTiming protocol properties.

Whopping answered 6/4, 2013 at 14:52 Comment(0)
C
0

Use KVC. setValue for Key for every animation. After that in animationDidStop you can define what animation has been stopped and run next in chain.

- (void)moveXrightAnimation {
    CABasicAnimation* theAnimation = ...
    [theAnimation setValue:@"movexright" forKey:@"animationID"];
//animation
}

- (void)rotate90leftAnimation {
    CABasicAnimation* theAnimation = ...
    [theAnimation setValue:@"rotate90left" forKey:@"animationID"];
//animation
}

- (void)moveXpointAnimation {
    CABasicAnimation* theAnimation = ...
    [theAnimation setValue:@"movexpoint" forKey:@"animationID"];
//animation
}

- (void)scaleAnimation {
    CABasicAnimation* theAnimation = ...
    [theAnimation setValue:@"scale" forKey:@"animationID"];
//animation
}

#pragma mark - CAAnimationDelegate

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    
    if([[anim valueForKey:@"animationID"] isEqual:@"movexright"]) {
        [self rotate90leftAnimation];
    }
    if([[anim valueForKey:@"animationID"] isEqual:@"rotate90left"]) {
        [self moveXpointAnimation];
    }
    if([[anim valueForKey:@"animationID"] isEqual:@"movexpoint"]) {
        [self scaleAnimation];
    }
}
Cousingerman answered 1/2, 2016 at 8:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.