Tutorial: Intro to Core Animation

Author: David Gohara
Web Site: gohara.wustl.edu

Core Animation, you've been hearing about how great it is, how it makes animation easier and how cool it is. Well you're about to find out first hand that all of the things you've heard are TRUE. We'll cover a little background first and then jump into a simple Cocoa example on how to build Core Animation functionality into your application using a couple of completely contrived examples (For those that want something a bit more advanced, I've posted some work-in-progress code, that shows how to use layering to create a cheap Cover Flow knock off. Hey it's a work in progress. See the end of the tutorial for links).

Core Animation... What it is and how it works (in a nutshell)

Core Animation is a technology that allows developers to produce smooth animated user interfaces. Some would argue it's purely eye-candy, but visual feedback is an important element of good UI design. Core Animation unloads a lot of the complexity in generating animated UI elements by providing an additional layer of abstraction between the programmer and underlying software/hardware interfaces.

If you are on Leopard (or have an iPhone or iPod Touch), you've seen examples of Core Animation already. Cover Flow is one example. For another more subtle example do the following:

1) Open any image file in Preview.app
2) Press the Zoom In button and watch the zoom process as it occurs

What you'll notice is that the image smoothly transitions from the original size to the new size. In Tiger you'd get one of two things.

1) If the engineer took the time to keyframe the transitions from one step to another you'd also get the smooth transition. He or She probably didn't because it was a lot of work to do this manually.

2) You'd notice that in going from one size to the other that it jumped instantly from the original size to the new one.

The difference between Tigers Preview zoom and Leopards is subtle, but very graceful. It's very Apple, if you will. And as you'll see, basic Core Animation transitions are dead simple to deploy (of course you can get extremely complicated for truly stunning effects, but that is not for this tutorial).

Ok. So how does Core Animation "work" ? That isn't so simple. But think of Core Animation as an extension of AppKit which is the applications framework that drives much of what you see on Mac OS X.


Core Animation sits between the programmer accessible elements of basic Cocoa animations and the underlying graphics hardware powered by OpenGL. It serves as part of the graphics unification layer which previously included QuickTime, Core Image and Quartz. Core Animation introduces a new concept to AppKit based views called "Layers."


Traditionally animation in Mac OS X is done via AppKit's Animation API's. With Core Animation views are backed by an animation layer, which is effectively a cached copy of the view (remember, buttons, sliders, image wells, windows etc... are all considered "views"). The cached layer is what is processed by the video hardware to produce the smooth visual effects we see in Core Animation enabled applications. If a view has a layer it is said to be "layer-backed."


Layer-backed views work in a hierarchy. Take for example an application Window with a button and table view. If that window is layer-backed, then by default the button and table view are also layer-backed. Each subview of a layer-backed view is layer-backed (by design). The converse isn't true. If we create a backing layer for our button, the application window isn't layer-backed. This is important to remember since if we forget and try to apply Core Animation to a non-backed view, nothing will happen.



At this point it might be tempting to conclude that you should always back the top most view in a rendering tree with a layer. If you do so, then all of your subviews get layers automatically. Right? However, it's important to remember that a layer is nothing more than a cached copy of a particular element. For each layer-backed view you have, there exists some allocation of memory (video memory) that is being occupied by that cached copy. So keep this in mind when setting up your layers. Back the elements you need, and leave the rest off (when convenient to do so) and conserve that video RAM for something else.

Two other things I should note is that Core Animation uses OpenGL for processing the rendering queue. But since almost all new Mac's ship with multiple CPU's/cores, Core Animation is threaded to take advantage of that as well. So while an animation is running your application can continue to do other things.

Animation

When we talk about animation we're talking about changing a property over time. As you'll see in the example below, we can vary a number of properties of views including height, width, rotation, opacity and color to name a few. In order to get smooth animation we have calculate the intermediate values between the initial and final states and draw them accordingly. This can be complicated and messy.

Core Animation takes a lot of the work out of the equation for simple animation types. Core Animation introduces a new type of proxy in communicating information about visual changes in state. This object is called the "animator" and each subclass of NSObject has one. When we want to performa a Core Animation transition from one state to another, rather than tell the object of interest what its new properties are, we tell the animator and the animator will then do all of the work for us. Again, that work includes determining all of the relevant state changes of the object to get from state A to state B.

For example, let's say we have a method called changeAlpha and it changes the opacity of a view element (an NSImageView). Traditionally we'd do it like so:


[imageWell setAlphaValue:0.5];


When the method is called, the alpha value will change, and it will do so in one step going from fully opaque to half transparent the next time the view is redrawn. Now with Core Animation we'll talk to the animator:


[[imageWell animator] setAlphaValue:0.5];


We first retrieve the imageWell's animator and tell it to set the alpha value. It will then interpolate between the current value (1.0) and the new value (0.5) in a series of steps over the default duration of time (0.25 seconds, unless changed).

Let's Animate...

You can download the completed project we'll discuss here. Go ahead... I'll wait.

Open up the Xcode project and press Build and Go. When the application launches you'll see an NSImageView with a picture of our good friend Bubb Rubb. Click the Shift button (don't mess with the Rotate just yet). You'll see the image shift from left to right click it again to send it back. Now, select "Use Core Animation" and click Shift again (again, don't mess with Rotate). You'll see the image view move from left to right. But smoothly this time. The difference between the two modes is one line of code (seriously).

Ok. Go back to Xcode and click on the file CAController.m. That is all of the code that creates and drives the project. Look at the IBAction method "shift:".

Here is the listing:


- (IBAction)shift:(id)sender
{
	
	int mult = [sender intValue] ? 1.0 : -1.0;
	
	//Set the frame position origin on x +/- 200 
	//depending on the current state
	position.origin.x += (mult * 200);
	
	//Now set the frame origin
	if([useCoreAnimation state] == NSOffState)
		[view setFrame:position];
	else
		[[view animator] setFrame:position];

}

The first two lines in the code create a multiplier based on the Shift button state and set the frame origin of the NSImageView. This is basic code. The important stuff is below that. Normally if we want to move the frame of our NSImageView (called view) we'd do something like this:


[view setFrame:position];


Simple. In doing so, the frame would be repositioned and redrawn. But it would happen all at once. Going from point A -> point B in one hop. Now if the "Use Core Animation" switch is checked, we do this instead:


[[view animator] setFrame:position];


Rather than talking directly to the view, we talk to the view's animator. Think of the view as a puppet and the animator the puppeteer. The animator controls how the view is transitioned from point A -> point B. You can control aspects of the animator by modifying the NSAnimationContext. For example if we wanted a slow transition we could do something like:


[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:3.0f];

[[view animator] setFrame:position];

[NSAnimationContext endGrouping];


Try adding that to the else block and see what happens. The NSImageView should transition more slowly.

Ok. Now let's look at Rotate, you know the one I didn't want you to mess with. It's pretty much the same code one line of code except this time we are setting the frames rotation when Core Animation is enabled.


[[view animator] setFrameRotation:[rotate floatValue]];


Go back to the application and do the following:

1) Enable Core Animation and press Shift
2) Now turn the Rotate dial and let go.

Bubb smoothly rotates from one angle to another. Now... Let's get to something important that I didn't take into account in this code. If you rotate Bubb a little bit, press shift, rotate him again a bit, press shift... do this a few times and you'll notice that the NSImageView size changes and distorts in odd ways. This is because in all of this code, we haven't taken into account the effect we are having on the views transform. We'll cover transforms at a later date, but for now, keep this in mind when performing complex operation types. If you view starts to distort you need to think about your transforms...

One thing I glossed over is how to enable Core Animation for our project. Remember that we need a layer backed view (and that all subviews of a layer backed view are automatically backed). So open the Resources disclosure triangle in the Xcode project, and double-click on MainMenu.nib. This will fire up Interface Builder.

In IB, press command-shift-I and double-click the Window icon (or if it's already open select the actual Window, the one with the view and buttons). In the Animation tab you should see:


That little checkbox means that the main application window is going to be layer backed (and hence any subview of that Window, which includes all controls, fields, data views etc...) will be layer backed as well, by default (even if they aren't checked).

You can also do this programatically, and hence on-the-fly by calling the -setWantsLayer method on an object:


[view setWantsLayer:YES];


Again, it's really that simple. In this case we've told the NSImageView that it and its subviews should have layers. In IB, we set the main application window (and hence its subviews, including the NSImageView) to have layers.

That's it for our Intro to Core Animation. Like I mentioned above a more complicated example/work in progress can be found in the FileBrowser example which can be downloaded here. This one programmatically shows how to set up layers and organize them to create a Cover Flow like effect with images. There is also a thread that describes how to access the private API that Apple uses for Cover Flow here. Beware, Apple can change private API's at any time, so don't use them in a shipping application.

Next time we'll talk about Core Animation classes and talk about how to use them to create more advanced animations and transitions. In the meantime additional information and examples of Core Animation can be downloaded from the Apple Developer Website (ADC account required).

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

NSBezierPaths and Core Animation?

Thank you, David, for the succinct tutorial. I notice that the smooth zoom in Preview.app works on .jpg and other image files, but not on PDF files, which continue to jump to the new resolution discontinuously despite the fact that vector graphics can be scaled in a continuous way. Does that mean that Core Animation cannot be used on views drawn by NSBezierPaths?

I am developing a finite-volume fluid dynamics simulation in which the (polygonal) cells are represented by NSBezierPaths that are then filled with a color to reflect the value of the scalar field (eg. vorticity) being diplayed. Currently the view updates discontinuously after a fixed number of time steps. Core animation seems to offer the possibility of a continuously updating view, with the field linearly interpolating between steps. But not with NSBezierPaths?

Re: NSBezierPaths and Core Animation?

My guess is that it doesn't (but that's just a guess). I'm basing this on the documentation in that NSBezierPath does not conform to NSAnimatablePropertyContainer (which things that are animatable like NSView's do, for example). Again though that's just a guess based on the API reference.

I do know that NSBezierPath's can be used IN Core Animations to lock layers to a specific spline. I'd need to look over the examples again to be sure about animatible properties. I think I have an example somewhere that does this, I'll try and dig it up.

Dave

NSBezierPath and NSView

Actually, if the NSBezierPath is drawn inside the drawRect: method, it should work too when you call the setRect: or setSize: method on the animator, e.g. [[myView animator] setRect:newRect]. Then the Core Animation code will call drawRect: at different intermediate Rect or Size, which will result in the NSBezierPath being redrawn smoothly.

About PDF in Preview: while PDF are similar to NSBezierPath, they are not the same thing, and do not go through the same classes. PDF drawing is much more efficient than NSBezierPath, and is also better looking, for text drawing in particular.

IB resources

Hi there

Very nice and simple run through the Core Animation Basics.

Just a quick question if you don't mind. When I downloaded your example code and went into IB I couldn't click on any of your controls until I turned the content view wants animation off. Also when I kept this option off and run you program the animation still happened. Is there something that I'm missing here.

I also tried to download your coverflow example and when I clicked the Load Images button the app just hanged.

I'm using a MacBook Pro with 10.5.1 installed.

Thanks

the app hangs because it is

the app hangs because it is calling cfrelease on a cfimage it does not own

in

- (CGImageRef) imageRefForResource:(id)resource ( in FBView.m)

add

++if(source)
++ {
img = CGImageSourceCreateImageAtIndex(source, 0, NULL);
CFRelease(source);
++ }

the source cant be read because the users directory is hardcoded to the developers dir which is not going to work unless you are him. the best thing to do would be to include the images in the resource folders of the app bundle or ask the user for the path - instead the developer hardcoded the folder the app is in. a quick and dirty solution - to get it working - is to replace the hard code in

- (FBLayer *)createSublayerFromPath:(NSString *)theObject

with

container.filePath = [NSHomeDirectory() stringByAppendingString:@"/Desktop/FileBrowser/images"];

and move the folder to the desktop. better solutions would require changes to the xcode project.

The app hangs

Thanks for that, it works fine now.

My Mac still seems to be having problems knowing when Core Animation is turned on or off and some downloaded examples work before the magic switch is even added. Strange I know.

Some comments and notes

First, I'm always thrilled to see people working with Cocoa Animation and Core Animation. But I'm also a stickler for paperwork.

This is, in fact, Cocoa Animatable Proxy Animation. With the exception of transitions, most all of these will work without Core Animation turned on for a layer (which we refer to as a layer-backed view).

When you drop down to accessing the layers directly, then you're really using Core Animation directly. And then you have Core Animation layer backing turned on, but those views are referred to as layer-hosting views.

The tell is this:

layer-backing views - you turn on wantsLayer and manipulate the view directly, animating through the animator
layer-hosting views - you create a layer, set it on the view, and then enable wantsLayer. You can resize the view, and reposition it, but all the drawing should be handled by the underlying layers directly.

Sadly the Cocoa Animation documentation hasn't been property updated for Leopard. It is on the list though, and the release notes are fantastic.

Only two objects support the animatablepropertycontainer protocol NSWindow and NSView.

You can animate along a path using explicit animation (CAKeyframeAnimation). but there is no animation proxy for bezier paths etc..

When to use layer-backed views, and when to use layers directly?

This is an interesting post, thanks.

I must admit that I am still struggling with this stuff, in particular, when I should use layers directly to, say, build a new control, and when I should use views/subviews that are layer-backed. What are the advantages and disadvantages to each approach?

For example, it seems to me that views are better in the sense that they are responders, and so have all the event stuff built in. Layers seem to be used more for 3D animations, which are less interactive (eg coverflow, appletv intro).

Are layers suited to event handling? Can you do anything with a layer that you can do with a view, or are they only suited to particular tasks?

Thanks again,
Drew

---------------------------
Drew McCormack
http://www.maccoremac.com
http://www.macanics.net
http://www.macresearch.org