bdunagan

Brian Dunagan

April 26 2009
Core Animation on the Mac

Core Animation is a fantastic tool in the Cocoa toolkit. It allows developers to add smooth visual animation to their Mac applications. I started playing with Core Animation after listening to Scotty's Late Night Cocoa podcast, part of the Mac Developer Network, specifically the episode with Bill Dudney. Check out his book, Core Animation for Mac OS X and the iPhone, at Pragmatic Programmers. And be sure to look at the Mac Developer Network; Scotty's podcasts are awesome.

While Core Animation is generally amazing, it's especially helpful for giving the user feedback for their actions. Clicking a button can now easily show an animation that tells the user what the button is doing. To illustrate this, I'm going to walk through two simple animations: a flying icon and a yellow fade. Below is a Quicktime movie showing the two animations, and at the bottom of the post is the code behind them.

First, I setup the skeleton application. I have a source list on the left with a couple entries and child entries. I hooked this view up with a simple NSOutlineView and an NSTreeController. On the far right, there is a button to start the animation. See the screenshot below.

Icon Animation

Clicking the button fires the selector animateButton:. That method does a couple things. First, it figures out where the button is and where it wants the icon to go, including the two curve points for the bezier path. Next, it calculates the actual bezier path the icon will follow. Then, it defines the animation, setting a duration and supplying the delegate and the path. Finally, it assigns that animation to the relevant NSView and starts the animation by calling setFrameOrigin:. The one important subtlety that is in [CAKeyframeAnimation animationWithKeyPath: @"frameOrigin"];. I found Core Animation to be very finicky about what key path I used to associate the animation with the view; frameOrigin works consistently, so try it if other key paths don't seem to work.

- (IBAction)animateButton:(id)sender {
	// Get the relevant frames.
	NSView *enclosingView = [[[NSApplication sharedApplication] mainWindow] contentView];
	int rowIndex = [sourceList selectedRow];
	NSRect cellFrame = [sourceList frameOfCellAtColumn:0 row:rowIndex];
	NSRect buttonFrame = [button frame];
	NSRect mainViewFrame = [enclosingView frame];
	
	/*
	 * Icon animation
	 */

	// Determine the animation's path.
	NSPoint startPoint = NSMakePoint(buttonFrame.origin.x + buttonFrame.size.width / 4, buttonFrame.origin.y + buttonFrame.size.height / 4);
	NSPoint curvePoint1 = NSMakePoint(startPoint.x, startPoint.y + 100);
	NSPoint endPoint = NSMakePoint(cellFrame.origin.x, mainViewFrame.size.height - cellFrame.origin.y - cellFrame.size.height);
	NSPoint curvePoint2 = NSMakePoint(endPoint.x + 200, endPoint.y);

	// Create the animation's path.
	CGPathRef path = NULL;
	CGMutablePathRef mutablepath = CGPathCreateMutable();
	CGPathMoveToPoint(mutablepath, NULL, startPoint.x, startPoint.y);
	CGPathAddCurveToPoint(mutablepath, NULL, curvePoint1.x, curvePoint1.y,
						  curvePoint2.x, curvePoint2.y,
						  endPoint.x, endPoint.y);
	path = CGPathCreateCopy(mutablepath);
	CGPathRelease(mutablepath);

	// Create animated icon view.
	NSImage *icon = [button image];
	[animatedIconView release];
	animatedIconView = [[NSImageView alloc] init];
	[animatedIconView setImage:icon];
	[animatedIconView setFrame:NSMakeRect(startPoint.x, startPoint.y, 20, 20)];
	[animatedIconView setHidden:NO];
	[enclosingView addSubview:animatedIconView];

	// Create icon animation.
	CAKeyframeAnimation *animatedIconAnimation = [CAKeyframeAnimation animationWithKeyPath: @"frameOrigin"];
	animatedIconAnimation.duration = 1.0;
	animatedIconAnimation.delegate = self;
	animatedIconAnimation.path = path;
	animatedIconAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
	[animatedIconView setAnimations:[NSDictionary dictionaryWithObject:animatedIconAnimation forKey:@"frameOrigin"]];

	// Start the icon animation.
	[[animatedIconView animator] setFrameOrigin:endPoint];
}

Yellow Fade Technique

A couple years ago, 37 Signals popularized a user feedback animation called the Yellow Fade Technique. When a user performs an action or the (web) application updates, the new information's background flashes yellow and then fades away.

Clicking the button fires the selector animateButton:. This time, that method does a few different things. First, it creates an NSView and gives it a CALayer; the delegate is set to the controller, so that the controller can draw the layer. Next, it defines the animation. This animation is a bit more complicated. It's actually a group of animations. Since I want the yellow overlay to flash twice, I define four animations: fade in and out once then fade in and out again. Perhaps the code could be tighter, but this approach works. Finally, I assign the animation group to the NSView and start it with an NSView call to setFrame:. Notice that I use frameOrigin for the animation's key path, even though I start the animation with setFrame:. Again, Core Animation is finicky.

I have an additional method defined for drawing the layer's content: drawLayer:. In this method, I draw a dark yellow curved rectangle around the cell frame and then fill the cell frame with a lighter yellow.

- (IBAction)animateButton:(id)sender {
	// Get the relevant frames.
	NSView *enclosingView = [[[NSApplication sharedApplication] mainWindow] contentView];
	int rowIndex = [sourceList selectedRow];
	NSRect cellFrame = [sourceList frameOfCellAtColumn:0 row:rowIndex];
	NSRect buttonFrame = [button frame];
	NSRect mainViewFrame = [enclosingView frame];

	/*
	 * Yellow fade animation
	 */

	// Create the yellow fade layer.
	CALayer *layer = [CALayer layer];
	[layer setDelegate:self];
	yellowFadeView = [[NSView alloc] init];
	[yellowFadeView setWantsLayer:YES];
	[yellowFadeView setFrame:cellFrame];
	[yellowFadeView setLayer:layer];
	[[yellowFadeView layer] setNeedsDisplay];
	[yellowFadeView setAlphaValue:0.0];
	[sourceList addSubview:yellowFadeView];

	// Create the animation pieces.
	CABasicAnimation *alphaAnimation = [CABasicAnimation animationWithKeyPath: @"alphaValue"];
	alphaAnimation.beginTime = 1.0;
	alphaAnimation.fromValue = [NSNumber numberWithFloat: 0.0];
	alphaAnimation.toValue = [NSNumber numberWithFloat: 1.0];
	alphaAnimation.duration = 0.25;
	CABasicAnimation *alphaAnimation2 = [CABasicAnimation animationWithKeyPath: @"alphaValue"];
	alphaAnimation2.beginTime = 1.25;
	alphaAnimation2.duration = 0.25;
	alphaAnimation2.fromValue = [NSNumber numberWithFloat: 1.0];
	alphaAnimation2.toValue = [NSNumber numberWithFloat: 0.0];
	CABasicAnimation *alphaAnimation3 = [CABasicAnimation animationWithKeyPath: @"alphaValue"];
	alphaAnimation3.beginTime = 1.5;
	alphaAnimation3.duration = 0.25;
	alphaAnimation3.fromValue = [NSNumber numberWithFloat: 0.0];
	alphaAnimation3.toValue = [NSNumber numberWithFloat: 1.0];
	CABasicAnimation *alphaAnimation4 = [CABasicAnimation animationWithKeyPath: @"alphaValue"];
	alphaAnimation4.beginTime = 1.75;
	alphaAnimation4.duration = 0.25;
	alphaAnimation4.fromValue = [NSNumber numberWithFloat: 1.0];
	alphaAnimation4.toValue = [NSNumber numberWithFloat: 0.0];

	// Create the animation group.
	CAAnimationGroup *yellowFadeAnimation = [CAAnimationGroup animation];
	yellowFadeAnimation.delegate = self;
	yellowFadeAnimation.animations = [NSArray arrayWithObjects: 
									   alphaAnimation, alphaAnimation2, alphaAnimation3, alphaAnimation4, nil];
	yellowFadeAnimation.duration = 2.0;
	[yellowFadeView setAnimations:[NSDictionary dictionaryWithObject:yellowFadeAnimation forKey:@"frameOrigin"]];

	// Start the yellow fade animation.
	[[yellowFadeView animator] setFrame:[yellowFadeView frame]];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
	// Bezier path radius
	int radius = 4;

	// Setup graphics context.
	NSGraphicsContext *nsGraphicsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:NO];
	[NSGraphicsContext saveGraphicsState];
	[NSGraphicsContext setCurrentContext:nsGraphicsContext];

	// Convert to NSRect.
	CGRect aRect = [layer frame];
	NSRect rect = NSMakeRect(aRect.origin.x, aRect.origin.y, aRect.size.width, aRect.size.height);

	// Draw dark outside line.
	[NSBezierPath setDefaultLineWidth:2];
	NSBezierPath *highlightPath = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius];
	[[NSColor yellowColor] set];
	[highlightPath stroke];

	// Draw transparent inside fill.
	CGFloat r, g, b, a;
	[[NSColor yellowColor] getRed:&r green:&g blue:&b alpha:&a];
	NSColor *transparentYellow = [NSColor colorWithCalibratedRed:r green:g blue:b alpha:0.5];
	NSBezierPath *fillPath = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius];
	[transparentYellow set];
	[fillPath fill];

	// Finish with graphics context.
	[NSGraphicsContext restoreGraphicsState];
}

BDCoreAnimation

Combine these two animations together for a nice icon swooshing into the source list with the source list item flashing yellow when the icon finishes. Check out the project (Mac OS X 10.5, Xcode 3.1.2) at BDCoreAnimation on Google Code.

For a more in-depth tutorial on Core Animation, please check out Bill Dudney's Core Animation for Mac OS X and the iPhone.

Leveraging setObjectValue: in an NSTableView Core Animation on the iPhone
LinkedIn GitHub Email