Apple’s Keynote has a cool and efficient UI paradigm on application start-up. It displays a panel with a collection of themes from which to start a new presentation, but also has a button on the panel to choose an existing presentation. Pressing the choose existing presentation button performs a panel flip animation to reveal the common system open panel. Canceling from the open panel results in a flip animation back to the theme chooser panel.
Start-up screens are ubiquitous, but I haven’t seen the addition of the open existing button before. It’s a small thing, but it saves the user the annoyance of having to hit cancel and then File/Open. The flip has also got that cool gratuitous Apple bling thing going on.
I’d implemented the flip effect using Core Animation previously in the context of a small item embedded in a larger super view, but that was flipping
NSWindows. Also, we’re talking about messing with modal windows to boot, including one that isn’t real sub-class friendly:
A quick consultation with Google turned up two different existing implementations of flipping
Both look well implemented and are conveniently packaged up as a category on
NSWindow. Both seemed to be showing their age a little though as they chug based on their image manipulation technology. It also wasn’t immediately obvious how to get either of them to work easily with
I decided to take a (very) similar approach to the existing implementations, but to use Core Animation to do the flipping for me and to tweak the setup and tear down to work better with the modal system panels. The key idea I took away from the previous implementations was creating an over-sized, non-opaque window in which a screen shot of the window being flipped would be animated.
The code for creating an over-sized non-opaque window is very straightforward:
NSRect overSizedBounds =
NSInsetRect([windowToFlip frame], -100, -100);
[[NSWindow alloc] initWithContentRect:overSizedBounds
Taking a screen shot of the existing window also turns out to be pretty easy:
NSView* view = [[[self window] contentView] superview];
NSRect bounds = [view bounds];
NSBitmapImageRep* bitmap =
Displaying the screen shot in the over-sized window using Core Animation is neat and clean. I used
setWantsLayer to request the content view of my over-sized window be layer backed, and then created a new
CALayer and added it as a sub-layer to the over-sized window.
CALayer avoids having to mess with layer backing the
NSView, which really doesn’t like to be messed much with. There’s probably some room for potential improvement in moving away from a layer backed view. The sub-layer holds the screen shot of the window being flipped, which thanks to the contents property of
CALayer and the
[NSBitmapImageRep CGImage] convenience method is also dead simple to implement:
NSView* flipView =
[[NSView alloc] initWithFrame:bounds];
_flipLayer = [CALayer layer];
[[flipView layer] addSublayer:_flipLayer];
[_flipLayer setContents:(id)[bitmap CGImage]];
The flipping code is just a
CABasicAnimation that animates
transform.animation.y from zero to PI/2. The only minor gotcha to remember is to set up perspective in the transform matrix
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0 / -850;
What I spent most of my time on was working to eliminate flicker. Initially, I assumed I could just wrap any transitions between the “real” windows and the “fake” over-sized window with calls to
While those calls were definitely a big piece of the puzzle, I also had to be careful to force the initial display of the window (
[NSWindow display]) to occur while updates were disabled. I also had to pay attention to the relative window levels as I was dealing with modal sheets. Both of these ideas are present in the two example programs cited earlier, but the modal sheets, timing and granularity of the delegates from
CABasicAnimation took some tweaking.
The last problem I encountered in replicating the silky smooth flip of Apple’s Keynote start-up template screen was some hitching around the point of transition due to setup and tear down cost. Again the solution was present in the example programs cited earlier, but I had to futz around with it a little based on Core Animation and modal timings and delegates.
In short, the solution was to use
setAlphaValue: to hide the latency of window creation, screen shot creation and the setting of the contents of the
CALayer. After the flip is completed from panel “A” to panel “B”, and panel “B” is presented to the user I perform the setup of the overlay window in anticipation of the next flip. This means I take a screen shot of panel “A” (without actually displaying it to the screen) and set the over-sized window up (also without displaying it to the screen), but setting it’s alpha value to zero so it remains invisible until it’s needed.
It’s easy to notice a hang in the middle of the transition animation, but it’s much harder to notice that the app is hanging at the end of the flip animation. In the latter case, the hang “hides” in the time it takes to visually parse the newly presented contents.
Big caveat: I’m targeting Leopard only and it’s not immediately obvious to me which aspects of the approach I took would fail on which previous versions of OS X. Both of the previously cited examples are approaches that work on past versions of the OS and older hardware, and seem like a good way to go if that kind of support is needed.
Hmm, now what important feature was I supposed to be implementing instead of working on eye candy? Flippin’ !#@$% modal panels.