1479 words on Mac OS X 10.5 Leopard
Mac OS X.5 comes with the new Core Animation technology. It aims to take the headache out of animating things on screen. And within its limited scope it does just that.
As long as you have a bunch of readymade media which you want to move around or distort on screen, Core Animation will be great. Essentially you get a bunch of layers in your views within which you can easily animate readymade graphics. While the graphics need to be 2D, you can even do those animations in 3D style and you’ll have all the filtering capabilities of Quartz within a line of code’s reach. In particular you won’t have to worry about low level stuff like Open GL to get the right graphics effects or about threads and timers to keep things moving smoothly.
Standard examples for using Core Animation may be things like smooth resizing of views, views fading in or out or things moving around on screen in CoverFlow style. Core Animation should be a great technology for adding such niceties which make things appear or disappear smoothly - rather than just toggling between discrete modes - simply because it greatly reduces the effort needed to implement them. Frequently a few mouse clicks and a few lines of code will be all that is required.
This advantage could also end up being a disadvantage, though. Simply because it makes animating things too easy; thus encouraging people to make user interfaces overly animated with animation ending up as a distracting extra that gets in the way of using the application rather than something helping the interaction. An ugly example of that can be seen when clicking the ‘Dynamic global host name’ check box in the sheet for computer name settings in X.5’s Sharing preferences. But I’m sure people will come up with much worse stuff.
Core Animation further encourages such unfortunate and overly animated application behaviour by making animated transitions the default. If you have a CALayer and change its opacity to hide it,
myLayer.opacity = 0.0;it will fade out within a time interval that is determined by Apple’s engineers (and, I presume, potentially changing in case Apple’s engineers change their mind). If you want the layer to hide right away you need to manually set that up with additional and clumsy code
[CATransaction begin]; [CATransaction setValue:[NSNumber numberWithFloat:0.0 forKey:kCATransactionAnimationDuration]; myLayer.opacity = 0.0; [CATransaction commit];I don’t consider that particularly elegant. And I think it overly encourages making things animated simply because it’s the default behaviour you get for typing less.
There’s no better way to learn about things than actually using them. Which is how I met - and clashed with - Core Animation. When implementing Symmetries, it seemed natural to use Core Animation. The application’s on-screen display consists of different layers:
For that alone having a convenient way of splitting things up into layers some of which (e.g. the guide layer) can be shown and hidden on demand seemed a very cool thing. As the demo and animation modes also animate the graphics on screen, the original plan was to use Core Animation for that as well. Unfortunately most of those plans failed and only the help layer is displayed by Core Animation in the final application. The following sections contain a few lessons I learned while doing this. Feel free to learn from them or to mention a better way of doing the same thing in the comments.
While it’s dead simple to move Core Animation layers around on screen, setting the content of those layers seems a bit odd at first. Layers do have a content
property which can be used to set the layer’s content to a CGImageRef
, i.e. a data type which seems rather foreign in Cocoa. You can convert an NSImage
to a CGImageRef
with a few lines of code and I think the comment in the NSImage
category found in Scott Stevenson’s simple ArtGallery Core Animation demo project expresses this well:
- (CGImageRef) cgImage { // I'm open to better ideas. :) NSData* data = [self TIFFRepresentation]; return CreateCGImageFromData(data); } // from http://developer.apple.com/technotes/tn2005/tn2143.html CGImageRef CreateCGImageFromData(NSData* data) { CGImageRef imageRef = NULL; CGImageSourceRef sourceRef; sourceRef = CGImageSourceCreateWithData((CFDataRef)data, NULL); if(sourceRef) { imageRef = CGImageSourceCreateImageAtIndex(sourceRef, 0, NULL); CFRelease(sourceRef); } return imageRef; }This way of doing things gets the job done, but it is expensive. A fact which may not matter if you only need to set the
content
of your layers once but if you want to display a custom animation, this easily halves the frame rate you get and will cost you plenty of RAM if you’re garbage collected because there’ll be plenty of huge stale objects hanging around before the next collection cycle.
Thus, if performance does play a role for you, you probably want to set up a delegate object for the layer in question and implement the -drawLayer:inContext:
method there. This lets you draw things straight into the correct graphics context and will not create all those superfluous objects on the way. To make things ‘easier’, it seems you need to pimp the CoreGraphics graphics context to give a Cocoa graphics context before drawing:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { [NSGraphicsContext saveGraphicsState]; NSGraphicsContext * graphicsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:NO]; [NSGraphicsContext setCurrentContext:graphicsContext]; /* YOUR DRAWING CODE HERE */ [NSGraphicsContext restoreGraphicsState]; }
In short: This does work in the end, but it may not be the most obvious thing.
As mentioned in the previous section, using the wrong implementation can easily degrade your performance significantly.
While moving from setting the content
property to implementing -drawLayer:inContext:
in the delegate doubled my frame rate (from 25 to 50 frames per second), the result still wasn’t good. Somehow the updates felt nowhere as immediate as those I got when drawing without Core Animation (at 60 frames per second). I don’t think that the extra frames per second played a relevant role there, but rather it seemed to be some delay coming from Core Animation itself.
I have no idea how to measure this, but I certainly ‘felt’ the difference and so did some of my testers. Which led me to think that Core Animation may not be the right tool for timely screen updates where you want cursor movements and redraws to be matched as tightly as possible.
Quite likely these delays don’t play a huge role when doing ‘imprecise’ Cover Flow-style interfaces or screensaver, but they were certainly a deal-breaker for me. Hence, Core Animation was out of the game for performance reasons.
This wasn’t entirely clear to me from the beginning: Core Animation is severely limited in what it can animate. The ‘animatable properties’ are restricted to properties related to layer positioning, appearance and the effects applied to layers. In particular - despite its name -, Core Animation is not made to animate your own arbitrary properties.
Allegedly Core Animation was called LayerKit before marketing got their dirty fingers on it. And seeing this limitation of Core Animation, makes the original name seem much more precise and less filled with broken promises. The framework is a tool for juggling layers around and not for animating arbitrary things.
Luckily, there is also the NSAnimation
class which was introduced in Mac OS X.4. It ended up being the star I needed to save the day when realising that the magic of Core Animation doesn’t suffice to make the Demo and Animation in Symmetries work.
With all these problems, I ended up using traditional Cocoa view drawing in Symmetries, used NSAnimation
to do the interesting animations and all that was left over for Core Animation to do was the Demo mode where panels with help text slide in above the main view. As should be clear by now that’s pretty much what Core Animation was made for, so it was reasonably easy to get it to work.
Yet, even this wasn’t 100% as simple as I would have liked it to be. My help pages slide in one by one, and it took a bit of care to make sure that resizing the window doesn’t reveal things it shouldn’t. Likewise, I found the positioning and scaling magic Core Animation uses somewhat confusing and it’s still not really clear to me whether it’s my or the system’s responsibility to trigger the drawing of a layer. Things didn’t work quite as automatically as I expected them to there.
Hi, I came here after having read your comments to the CA-post on theocacao.com. Even after having read this post here, I don’t quite get you point: Especially why would you want to repeatedly update the content image during animation? It should be quite obious that this will really degrade performance…
Where is the problem with CA directly using Quartz 2D for drawing? Which btw. is correctly stated in the introduction of the programming guide…
D