Advertise here

Advertise here

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Using animation groups to create a linked sequence of animation steps

Duncan CDuncan C Posts: 9,112Tutorial Authors, Registered Users @ @ @ @ @ @ @
edited August 2013 in iPhone SDK Tutorials
Lately I've been reading up on Core Animation (CA), and learning how to use a wider range of it's features.

Our company develops for both Mac and iOS, so where practical, I like to use techniques that are the same across platforms.

The Mac OS flavor of CA doesn't support view animation, or the new block-based animations that are available in iOS. On the other hand, Mac OS objects like views and windows have an animator proxy object, and you can simply send messages to change properties to the proxy instead of sending them to the view, and the proxy creates an animation for you.

Both platforms support CAAnimation objects including CABasicAnimation and CAAnimationGroup.

On iOS, I've written cartoon style animations by using a mixture of view animation, CABasicAnimation, and CAKeyframeAnimation objects. That involves setting up completion callbacks that increment a counter and trigger the next step in the animation. There's a link to a tutorial in my sig that uses that approach, taken our of my company's "Kevin and Kell" cartoon reader.

I wanted to see if you could use animation groups to link a sequence of animations. It turns out you can, although there are some gotchas.

I created a project first in Mac OS, and then ported it over to iOS. The core of the code is the same between the two projects. the differences are that Mac OS uses NSView, and iOS uses UIViews, CGRects, CGPoints, etc. That was about all I needed to change to move the code from Mac to iOS.

The animation itself is fairly useless. It just animates our company logo, fading in, moving diagonally across the screen, rotating, falling (with an slide whistle sound), rotating back to it's original orientation, sliding sideways back to it's starting position, and then fading out again.

You can download the project at this link:

Creating a linked set of animations using a CAAnimationGroup

The core of the program is a routine called -doAnimation that triggers the animation sequence.

It creates a whole bunch of CABasicAnimation objects, each with a start time that is after the end of the previous animation. It then creates a CAAnimationGroup object and installs all the individual animations into that group. Its sets the duration of the animation group to the total time for all the component animations.

I created a variable that keeps track of the total time for all the animations so far, so it can figure out when to start the next animation in the sequence.

The animations are installed on the layer that backs the image view I am animating.

In order for each animation to build on the previous animation, you have to set fillMode = kCAFillModeForwards and removedOnCompletion = FALSE (which leaves the animation's effects in force once it's finished.) You have to do those steps for every animation in the group AND for the group animation. I tried setting removedOnCompletion to TRUE for one of the animations in the group, and it behaves very strangely.

I wanted to add a sound to the mix at a specific point in the animation sequence. Normally, with CABasicAnimation, you can set an object up as a delegate of the animation, and if it finds a animationDidStop:finished: method in the delegate, it will call it once that animation is complete. Apparently that doesn't work when an animation is part of a group. I think the reason is that the animationDidStop method is called "when the animation completes its active duration or is removed from the object it is attached to." In this case, the animation is part of a group, so it doesn't get removed from the group it belongs to.

The docs say that animations don't actually change the underlying properties that they animate (location, frame, opacity, transform, etc.) but only create the appearance of that property being changed. They instruct you to change the underlying value through code after submitting the animation if you want the layer to keep the changes applied by the animation. However, there's a gotcha. CAAnimations are added to layers, not view objects. By default, layers do an implicit animation when you change one of their values. Simply executing the statement
layer.opacity = 0.0;

Causes the layer to fade away. You have to use a block of code like this if you want a change to a layer setting not to animate:
[CATransaction begin];
  [CATransaction setValue: [NSNumber numberWithBool: YES]
    forKey: kCATransactionDisableActions];
  imageOne.layer.opacity = 0.0;
  [CATransaction commit];

Anyway, I will post all the code to the -doAnimation method in a reply to this thread (the forum won't let me post it here because the post would be too long. Sigh...)

One of the Core Animation books I'm reading says that you can actually use a CAAnimation object to animate properties in other objects than just the target for the animation. The only requirement is that you be able to build a key KVO expression that links to the target object (e.g. "parentView.sprite1View.legView.layer.position").

I had visions of targeting a group animation at a parent view, and then reaching into the subviews of the parent to animate crossfades, animate multiple views independently, etc. However, you can't add "run this animation now" animations to views. Only layers support that type of animation, and layers don't lend themselves to linking with KVO expressions like views do. In order to do the parentView.sprite1View.legView.layer.position" example I gave above, I would have to make most/all the views in the view hierarchy custom subclasses of UIImageView (or NSImageView on the Mac) and include properties that link to the other views. As far as I know, you can't create subclasses of layers, since layers are created by the system automatically for the views they are linked to.
Post edited by Duncan C on
Duncan C

Animated GIF created with Face Dancer, available for free in the app store.

I'm available for one-on-one help at CodeMentor


  • Duncan CDuncan C Posts: 9,112Tutorial Authors, Registered Users @ @ @ @ @ @ @
    edited August 2013
    Here is the code for the workhorse -doAnimation method from this project:

    - (IBAction) doAnimation: (id) sender;
      self.animationInFlight = TRUE;
      CGPoint oldOrigin;
      CGFloat duration = 0.4;
      CGFloat totalDuration = 0.0;
      CGFloat pause = 0.01;
      CGFloat start = 0;
      //First animate the opacity to 1.0
      CABasicAnimation* show =  [CABasicAnimation animationWithKeyPath: @opacity];
      show.removedOnCompletion = FALSE;
      show.fillMode = kCAFillModeForwards;
      show.duration = duration;
      show.beginTime = start;
      start = start + duration + .1;  //Calculate the start of the next animation.
      show.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
      [show setToValue: [NSNumber numberWithFloat: 1.0]];
      //Move the image up and to the right
      oldOrigin = imageOne.layer.position;
      CGPoint newOrigin = CGPointMake(oldOrigin.x + 150, oldOrigin.y - 250);
      CABasicAnimation* move =  [CABasicAnimation animationWithKeyPath: @position];
      move.removedOnCompletion = FALSE;
      move.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
      move.fillMode = kCAFillModeForwards;
      move.duration = duration;
      move.beginTime = start;
      start = start + duration + pause;
      move.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
      [move setToValue: [NSValue valueWithCGPoint: newOrigin]];
      //Rotate it 90 degrees
      CABasicAnimation* rotate =  [CABasicAnimation animationWithKeyPath: @transform.rotation.z];
      rotate.removedOnCompletion = FALSE;
      rotate.fillMode = kCAFillModeForwards;
      rotate.duration = duration;
      rotate.beginTime = start;
      //Setting an animation's delegate should cause the animation to call animationDidStop:finished
      //when it completes, but it doesn't seem to work when an animation is part of a group
      //rotate.delegate = self; //This doesn't work!
      start = start + duration + pause;
      [self performSelector: @selector(playDropSound) withObject: nil afterDelay: (NSTimeInterval)start];
      rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
      [rotate setToValue: [NSNumber numberWithFloat: -M_PI / 2]];
      //Move it down
      newOrigin = CGPointMake(newOrigin.x, newOrigin.y + 250);
      CABasicAnimation* moveDown =  [CABasicAnimation animationWithKeyPath: @position];
      moveDown.removedOnCompletion = FALSE;
      moveDown.fillMode = kCAFillModeForwards;
      moveDown.duration = 0.8;
      moveDown.beginTime = start;
      moveDown.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
      start = start + moveDown.duration + pause;
      [moveDown setToValue: [NSValue valueWithCGPoint: newOrigin]];
      //Rotate it back
      CABasicAnimation* rotateBack =  [CABasicAnimation animationWithKeyPath: @transform.rotation.z];
      rotateBack.removedOnCompletion = FALSE;
      rotateBack.fillMode = kCAFillModeForwards;
      rotateBack.duration = duration;
      rotateBack.beginTime = start;
      start = start + duration + pause;
      rotateBack.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
      [rotateBack setToValue: [NSNumber numberWithFloat: 0.0]];
      //Shift it back to the left
      CABasicAnimation* moveBack =  [CABasicAnimation animationWithKeyPath: @position];
      moveBack.removedOnCompletion = FALSE;
      moveBack.fillMode = kCAFillModeForwards;
      moveBack.duration = duration;
      moveBack.beginTime = start;
      moveBack.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
      start = start + moveBack.duration + pause;
      [moveBack setToValue: [NSValue valueWithCGPoint: oldOrigin]];
      //Finally, hide it again.
      start +=.2; //Wait a little longer before hiding the image.
      CABasicAnimation* hide =  [CABasicAnimation animationWithKeyPath: @opacity];
      hide.removedOnCompletion = FALSE;
      hide.fillMode = kCAFillModeForwards;
      hide.duration = duration;
      hide.beginTime = start;
      totalDuration = start + duration + pause; //Calc the total duration of all animations
      rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
      [hide setToValue: [NSNumber numberWithFloat: 0.0]];
      //Put all the animations into a group.
      CAAnimationGroup* group = [CAAnimationGroup animation];
      [group setDuration: totalDuration];  //Set the duration of the group to the time for all animations
      group.removedOnCompletion = FALSE;
      group.fillMode = kCAFillModeForwards;
      [group setAnimations: [NSArray arrayWithObjects: show, move, rotate, moveDown, rotateBack, moveBack, hide, nil]];
      [imageOne.layer addAnimation: group forKey:  nil];
      //Queue up a timer to do cleanup once the group animation is finished.
      [NSTimer scheduledTimerWithTimeInterval: totalDuration 
                                       target: self 
                                     selector: @selector(animationCleanup)
                                     userInfo: nil 
                                      repeats: NO];
    Post edited by Duncan C on
    Duncan C

    Animated GIF created with Face Dancer, available for free in the app store.

    I'm available for one-on-one help at CodeMentor
  • malaki1974malaki1974 Posts: 218Registered Users
    edited December 2011
    Exactly what I was looking for. Thank you.
  • justin-UKjustin-UK Posts: 58Registered Users @
    Just wanted to share an observation. If any of your animations include an autoreverses = YES command then you have to allow double the time in the begintime of the next anim and the overall group time. This may be obvious to some but has had me in knots!
Sign In or Register to comment.