Extra Credit 1: Animations

From AwkwardTV
Jump to: navigation, search


In the the second article in this series, we used a simple animation, the fade-in. The animations available can be more complex than that, however, even without creating a custom BRAnimation subclass. In this short walk-through, we'll replace the fade-in animation with a grander swooping image animation. As with the last article, a sample project will be included at the end.

Basic Animation Classes

The BackRow framework provides classes for value animations, frame animations, HUD animations (whatever they are), and 'aggregate' animations. Here we'll be looking at the value and frame, and aggregate animations.

Value Animations

BRValueAnimation uses key-value coding to animate a settable property/value within a given object. In this case, the object can be literally any object (it doesn't need to be a layer or control, as far as I can determine) and the only requirement is that the value is key-value-coding compliant. For instance, the fade-in animation essentially does the following:

[animation setKey: @"alphaValue"];
[animation setFromValue: [NSNumber numberWithFloat: 0.0f]];
[animation setToValue: [NSNumber numberWithFloat: 1.0f]];

So, we can animate other values with this, such as the reflection offset (using key @"reflectionOffset") and the Y-axis rotation (using key @"yRotation").

Frame Animations

BRFrameAnimation takes two 'parameters' -- a BRRenderLayer upon which to operate, and a target frame rectangle. The starting frame is the existing frame of the provided layer. Its design is such that you can take an existing layer and smoothly move it to its destination, using something like the following:

[animation setRenderLayer: myLayer];
[animation setTargetFrame: newFrame];
[animation setDuration: 0.8];
[animation run];

Aggregate Animations

BRAggregateAnimation is used to bundle together a list of other animation instances. This way we can animate more than one value at a time. The only thing of which to be careful is the aggregate animation's handling of durations. Firstly, it doesn't detect a duration from its contents, it must be set explicitly, as with all other animations. Secondly, the aggregate appears to order its contents such that they all end together rather than begin together.

Animations are added to an aggregate using the -addAnimation: message, like so:

[aggregate addAnimation: animation1];
[aggregate addAnimation: animation2];
[aggregate run];

Asynchronous Animation

By default, animations are synchronous; that is, when you call -run, they will perform the animation before that function returns. They can all be made asynchronous by calling -setAsynchronous: and passing YES. At this point, however, some questions arise:

  1. What happens if your controller is popped and/or deallocated while the animation is running?
  2. How do you find out when the animation ends in order to do something else?

The answers to both of those are:

  1. The application crashes (ouch).
  2. You need to register for the 'animation complete' notification.

To avoid the first problem, you will need to retain the animation and then tell it to stop when your controller is popped from the stack or is deallocated. For the second problem, you need to register to receive BRAnimationCompletedNotification, with the notification object being the animation you're interested in:

[[NSNotificationCenter defaultCenter] addObserver: self
                                         selector: @selector(animationFinished:)
                                             name: @"BRAnimationCompleteNotification"
                                           object: animation];

Putting It All Together

Now we'll make the changes to the controller we created last time. Our goal is to make the image swoop in from the left of the screen, turning from a 90 degree angle to face the viewer, and lifting off the ground a little as it arrives. It will then slide downward to settle on the ground. This means we will have to animate a number of different attributes, in a couple of stages.

First of all, we have the initial swoop inwards. Here we will have a straightforward value animation on the yRotation key, turning the image from 90 degrees to zero. We will also need a frame animation to move the image from a starting point designed to appear some way off until it reaches its original destination. The distance from the ground is altered by putting a value animation on the image's reflectionOffset key.

This will have to be done in two steps, though: we want the swoop to end with the image still in the air, at which point it will settle onto the ground. That second step will require a separate animation.

New Interfaces

So, we'll need two animations, a notification callback handler, and some means of identifying which animation we're on now. We'll add a new BOOL member variable called _doneFirstAnim, a new BRAnimation member variable called _animation, and the following three methods:

- (void) doFirstAnimation;
- (void) doSecondAnimation;
- (void) animationDone: (NSNotification *) note;


The -dealloc and -willBePopped methods will need to have a new line added to them:

[_animation stop];

The -dealloc method will also of course need to send release to the animation object.

Animation 1: Swoop

We'll need three different animation types: Value, Frame, and the Aggregate. We also want to get the image's frame (currently set to its settled position), so we can figure out a good place to start and end:

BRAggregateAnimation * anim = [BRAggregateAnimation animationWithScene: [self scene]];
BRValueAnimation * valanim;
BRFrameAnimation * frameanim;
NSRect frame = [_image frame];

The first animation we'll set up is on the reflection offset. This will stay static most of the way in, then change as it arrives at its destination, growing larger. Since the aggregate animation will tie the endings together, this means that we can simply make the duration a little shorter and it'll happen towards the end of the main swoop:

valanim = [BRValueAnimation animationWithScene: [self scene]];
[valanim setTarget: _image];
[valanim setKey: @"reflectionOffset"];
[valanim setFromValue: [NSNumber numberWithFloat: 0.4f]];
[valanim setToValue: [NSNumber numberWithFloat: 1.0f]];
[valanim setDuration: 0.5];
[anim addAnimation: valanim];

The second part is to animate the rotation of the image to face the viewer. This will take place across the entire duration of the aggregate animation:

valanim = [BRValueAnimation animationWithScene: [self scene]];
[valanim setTarget: _image];
[valanim setKey: @"yPosition"];
[valanim setFromValue: [NSNumber numberWithFloat: 90.0f]];
[valanim setToValue: [NSNumber numberWithFloat: 0.0f]];
[valanim setDuration: 1.5];
[anim addAnimation: valanim];

Lastly we'll tackle the frame animation. As discussed before, it picks up its starting point from the layer it's given, so we need to change the frame for our image to reflect that. We will also need to create the end frame, which will be slightly different from the image's current frame, a little way above its final resting place:

NSRect endFrame = frame;
endFrame.origin.y += endFrame.size.height * 0.5f;

The start frame will be smaller and way off towards the left of the screen:

float newHeight = frame.size.height * 0.5f;
frame.origin.x = 0.0f;
frame.origin.y += (frame.size.height - newHeight) * 0.6f;
frame.size.width = frame.size.width * 0.5f;
frame.size.height = newHeight;
[_image setFrame: frame];

Now we can build our frame animation:

frameanim = [BRFrameAnimation animationWithScene: [self scene]];
[frameanim setRenderLayer: [_image layer]];
[frameanim setTargetFrame: endFrame];
[frameanim setDuration: 1.5];
[anim addAnimation: frameanim];

All that remains is to set the properties of the aggregate animation, register for the completion notification, and run it:

[anim setAsynchronous: YES];
[anim setDuration: 1.5];
[[NSNotificationCenter defaultCenter] addObserver: self
                                         selector: @selector(animationDone:)
_animation = [anim retain];
[anim run];

Animation 2: Settle

In this animation we will change only the reflection offset and the frame, making the two meet in the middle. First we'll set up the reflection animation:

BRValueAnimation * reflect = [BRValueAnimation animationWithScene: [self scene]];
[reflect setTarget: _image];
[reflect setKey: @"reflectionOffset"];
[reflect setFromValue: [NSNumber numberWithFloat: [_image reflectionOffset]]];
[reflect setToValue: [NSNumber numberWithFloat: 0.0f]];
[reflect setDuration: 1.2];

Next we'll setup the frame animation. Remember that the end point of the first animation had the image at full size raised upwards by half its height, so we calculate our end frame using that assumption:

NSRect endFrame = [_image frame];
frame.origin.y -= frame.size.height * 0.5f;
BRFrameAnimation * loc = [BRFrameAnimation animationWithScene: [self scene]];
[loc setRenderLayer: [_image layer]];
[loc setTargetFrame: endFrame];
[loc setDuration: 1.2];

Now we an setup our aggregate, register for notifications from it, and run:

BRAggregateAnimation * lander = [BRAggregateAnimation animationWithScene: [self scene]];
[lander addAnimation: reflect];
[lander addAnimation: loc];
[lander setDuration: 1.2];
[lander setAsynchronous: YES];
[[NSNotificationCenter defaultCenter] addObserver: self
                                         selector: @selector(animationDone:)
                                             name: @"BRAnimationCompleteNotification"
                                           object: lander];
_animation = [lander retain];
[lander run];

The Notification Handler

Our last task for today is the notification handler. This will first of all make doubly sure that the animation posting the notification is really the one we're interested in. If that is the case, we need to remove the notification observer and release our hold on the animation. Then we check the _firstAnimDone flag, and conditionally run the second animation:

if ( [note object] != _animation )
[_animation autorelease];
_animation = nil;

[[NSNotificationCenter defaultCenter] removeObserver: self
                                                 name: [note name]
                                               object: [note object]];

if ( _firstAnimDone == NO )
    _firstAnimDone = YES;
    [self doSecondAnimation];

Note that we use [_animation autorelease] there to ensure that the animation object isn't released while it still has code executing on the stack.


So today we've seen the sort of interesting things that can be done with the basic animation classes in BackRow. In a future lesson I'll introduce the core principles used to run animations of various kinds, including animating and redrawing the contents of a layer as a function of time elapsed.

A sample project is available here.

Contact Details

You can find me at #awkwardtv on irc.moofspeak.net, username alan_quatermain, or at http://forum.awkwardtv.org/, username Quatermain.