Learn Objective-C, Building an App (Part 9): Quartz Demo 3
In this last section, we will combine the drawing abilities of Quartz with the blazing fast animations that are provided by Core Animation.
Core Animation Primer
Core Animation is a framework for animating a number of properties on views. It was introduced with OS X 10.5 (Leopard) and iPhone OS 3.0. Animation is important because it conveys visual feedback , especially in state change. For example, zooming in on OS X and iOS is an animated process, rather than jumping from one zoom level to another. This shows what happened, rather than providing a visual disconnect. Core Animation handles the animation implicitly, which means that, if you choose to accept the default options, you can simply set a property and the transition will be animated. Of course, you can also have fine-grained control of the animation.
Core Animation animations are fully GPU-backed and coded through OpenGL. This allows the animations to be incredibly fast—the original iPhone could hit 30fps on UI animations while Windows XP and earlier versions of Android performed all the animation code in the CPU, which is a significant performance bottleneck (the benefits of hardware accelerated graphics).
Core Animation exists as a backing layer behind your views. The layer is a cached copy of the view stored in the graphics card. It propagates down a view hierarchy—subviews of a view are automatically backed, but parent views are not automatically backed. It might be easiest to back the topmost view, but because each layer is stored in video memory (which may be shared with the main system memory), it is best to minimize your memory footprint. Back only the layers you need to animate.
Core Animation allows you to perform some styling options that are much simpler than using Quartz. For example, you can take an image, apply a styled border, round the corners, and put a drop shadow underneath—and animate all of it, using a few lines of Core Animation code.
Let’s get started.
General-Purpose Drawing with Core Animation
Open up CustomView.m in our sample project, and go to drawOtherInContext:. First we’ll look at how to draw a rounded rectangle in Quartz. This uses a method introduced with the iPad in iOS 3.2; it was not available on the iPhone until iOS 4.2, seven months later; Core Animation was introduced in iOS 3.0.
First, make sure to import QuartzCore.h in CustomView.h:
The Quartz code:
UIBezierPath *roundedRectQuartz = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(10, 10, 70, 90) cornerRadius:8.0];
[[UIColor orangeColor] setFill];
The Core Animation code:
UIView *roundedRectView = [[UIView alloc] initWithFrame:CGRectMake(90, 10, 70, 90)];
roundedRectView.backgroundColor = [UIColor orangeColor];
roundedRectView.layer.cornerRadius = 8.0;
Although the Core Animation code is actually a line longer, you get to work with standard UIKit interfaces; in fact, you could perform CA-type drawing on existing UIView elements (like buttons and text fields) without having to subclass them (remember that Quartz runs through the drawRect: method, so you’d have to subclass to use Quartz). The line of interest is the third one, where we access the layer property on the newly created view. This layer is a reference to a CALayer, the class the represents the CA backing layer. The cornerRadius is a built-in property of the class.
You can manipulate CALayers as you would UIViews. So you can create another layer and add it to your existing layer:
CALayer *shadowBox = [CALayer layer];
shadowBox.backgroundColor = [UIColor purpleColor].CGColor;
shadowBox.shadowColor = [UIColor blackColor].CGColor;
shadowBox.shadowRadius = 1.0;
shadowBox.shadowOpacity = 0.3;
shadowBox.shadowOffset = CGSizeMake(1.0, -2.0);
shadowBox.frame = CGRectMake(120, 30, 20, 30);
The results are exactly what you expect, but using easy Objective-C rather than C.
Finally, we’ll play around with an image:
CALayer *imageBox = [CALayer layer];
imageBox.backgroundColor = [UIColor blackColor].CGColor;
imageBox.borderColor = [UIColor whiteColor].CGColor;
imageBox.borderWidth = 3.0;
imageBox.cornerRadius = 10.0;
imageBox.shadowColor = [UIColor blackColor].CGColor;
imageBox.shadowRadius = 3.0;
imageBox.shadowOpacity = 0.8;
imageBox.shadowOffset = CGSizeMake(2.0, 2.0);
imageBox.frame = CGRectMake(180, 10, 102, 64);
CALayer *imageLayer = [CALayer layer];
imageLayer.contents = (id)[UIImage imageNamed:@"Image Fill.jpg"].CGImage;
imageLayer.cornerRadius = 10.0;
imageLayer.masksToBounds = YES;
imageLayer.frame = imageBox.bounds;
Here, we actually need to create two layers. To force the image to have rounded corners (by default it’ll draw the image regardless of the corners), you need to set the masksToBounds property to YES. This, however, prevents the shadow from being drawn, as the shadow is outside of the bounds. Therefore, you need a second layer to hold the image; the first will contain the border and shadow.
You can also perform Quartz-like custom drawing with CALayers as well. You need to set a delegate for the layer; the delegate must implement drawLayer:inContext:, which is analogous to drawRect:. You then call setNeedsDisplay on the layer, which works just as it does with UIViews.
Animating with Core Animation
Let’s look at a quick example:
CALayer *pulsingBox = [CALayer layer];
pulsingBox.backgroundColor = [UIColor whiteColor].CGColor;
pulsingBox.borderColor = [UIColor blackColor].CGColor;
pulsingBox.borderWidth = 2.0;
pulsingBox.cornerRadius = 5.0;
pulsingBox.frame = CGRectMake(10, 10, 80, 50);
CABasicAnimation *pulsingAnimation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
pulsingAnimation.toValue = (__bridge id)([UIColor orangeColor].CGColor);
pulsingAnimation.duration = 3;
pulsingAnimation.repeatCount = 10;
pulsingAnimation.autoreverses = YES;
[pulsingBox addAnimation:pulsingAnimation forKey:@"backgroundColorPulse"];
Here we create a CALayer as before. Then we create an instance of CABasicAnimation, which allows us to animate the value of a key path. We want this to pulse, so we only need to set an ending value; setting a starting value is usually redundant anyway. The (bridge id) bit is simply an ARC-specific cast of a struct type to id. We set the duration of the animation, a repeat count, and have it automatically reverse, which gives the pulse we’re looking for. We then add the animation to the layer, which implicitly causes it to start animating. The addAnimation:forKey: method takes a string as its second argument; this is the string that is used to identify the animation. To stop the animation before it’s finished, you call removeAnimationForKey:, using the same key. You can also send removeAllAnimations to stop all animations for a layer.
In this way, you can only animate one property at a time. You can combine multiple CAAnimation objects in a CAAnimationGroup object, which contains an array of CAAnimations. You then set the animation group as the animation on a layer, and all the properties animate. This is useful for setting the timing on a group; the timing of animations within the group are clipped to the timing of the group.
Motion Paths and Repetition
You can create much more complicated animations using keyframes. Keyframes are locations in the animation where you explicitely set the values of certain parameters, and the animation system will calculate all the intermediate steps based on the animation properties and the start and end values. In Core Animation, this is represented by CAKeyframeAnimation.
CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath: @"position"];
bounceAnimation.removedOnCompletion = YES;
bounceAnimation.fillMode = kCAFillModeForwards;
bounceAnimation.duration = 5;
bounceAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
We create our animation, tell it to remove itself after it’s done (to reduce processing and memory usage), retain the final position after it’s done (that’s what the fillMode specifies), make it take 5 seconds, and accelerate at the start and decelerate at the end.
Next, we have to define the path for the animation to follow (since we are animating the position). We create a mutable path object and draw to that, just as we would to a context. However, the functions we use have “path” in their names, rather than “context”. Finally, we assign the path to the animation.
CGMutablePathRef bouncePath = CGPathCreateMutable();
CGPathMoveToPoint(bouncePath, NULL, 0, 120);
CGPathAddArc(bouncePath, NULL, 0, 180, 60, 0.5*M_PI, 0, 0);
CGPathAddArc(bouncePath, NULL, 120, 180, 60, M_PI, 0, 0);
CGPathAddArc(bouncePath, NULL, 240, 180, 60, M_PI, 0, 0);
* CGPathAddArc(bouncePath, NULL, 360, 180, 60, M_PI, 0, 0);
Finally, we create a view to animate, and add the animation to the view.
UIView *animatingView = [[UIView alloc] initWithFrame:CGRectMake(0, 90, 48, 60)];
animatingView.backgroundColor = [UIColor redColor];
[animatingView.layer addAnimation:bounceAnimation forKey:nil];
We’ve gotten the view to animate, following a crazy path that we’ve defined. You’ll note that it’s not very smooth…but that comes down to the timing function. You can adjust the timing function just as you could the position of colors in a gradient. But that’s a topic for another time.
Sorry for the lack of images in this post: I lost a lot of data when a power surge knocked out my computer and much of my backup as well. All the screenshots I had were lost. But here’s the code from this project, so you can build and run at your leisure.
This post is part of the Learn Objective-C in 24 Days course.
Author: Feifan Zhou