With this article, I’d like to share some bite-sized pieces of information I’ve learned while working on some of my latest projects.
There’s also a sample project attached. Let’s start by introducing Core Animation.
Core Animation is responsible for almost everything you see on your iOS device’s screen. You’re probably familiar with UIKit, a lot of which is built on top of Core Animation: every UIView is backed by a CALayer. UIKit provides a high-level and powerful framework that can be used to do 80% of everything. If you’re reading this though, you probably think that’s not good enough.
With Core Animation, you can do everything UIKit does and much more. However, this comes at a price: you get a lower-level interface, which is harder to learn and to use effectively. In some circumstances you’ll need to know what the weak points of the hardware your app is running on are, just to get decent performance.
Core Animation is built on top of two even-lower-level frameworks: OpenGL and Quartz. Quartz is used for drawing/rendering 2D graphics. OpenGL takes care of compositing them, generating the scene that ultimately gets displayed - taking advantage of hardware acceleration.
So why write about it? The way I see it, Core Animation is one of the tools that allow you to take your app’s UX to the next level. It’s a key component in making apps beautiful, responsive, smooth, and realistic in the way they react to user input. Together, these attributes result in products that feel trustworthy, delightful and fun. And that’s exactly how they should feel.
This article is all about Core Animation, so I thought it would be nice to only use that API. As a result, there will be no drawing code - no
-drawRect: at all.
Core Animation’s workhorse! Think about it as a lower-level
UIView. Can display all kinds of content. Supports things like drop shadows and rounded corners out of the box. Almost all of its properties can be animated.
As I mentioned earlier, every
UIView has a
CALayer (you can get to it using the
layer property). Every layer can have any amount of sublayers, which are are very similar to subviews in UIKit.
A subclass of CALayer, specialized in rendering shapes. Whatever shape you can draw with a CGPath, CAShapeLayer can display.
shapeLayer.path = [UIBezierPath bezierPathWithOvalInRect:shapeLayer.bounds].CGPath;
Another subclass of CALayer that can display all kinds of axial gradients (a.k.a. linear gradients), with any number of stops and colors.
CALayers support masking: they have a
mask property that you can set to another CALayer. The alpha channel of the mask layer will determine what portion of the layer will be displayed.
Note, from Apple’s docs:
The layer you assign to this property must not have a superlayer. If it does, the behavior is undefined.
In other words, a layer shouldn’t be used as a mask for two (or more) layers. It just doesn’t work. Clone the original mask layer every time you want to reuse it.
Drop shadows are very easy to implement.
Just set these properties on any CALayer:
shadowColora shadow can be given any color. Do you want to make something glow? That’s just a white shadow.
shadowOpacitya number in the [0..1] range that does what you think it does.
shadowRadiusthis represents the strength of the blur effect that is applied to the shadow, and thus its perceived size. Defaults to 0.
shadowOffseta shadow offset of [5,10] will drop the shadow 5px to the right and 10px below your layer’s contents. Defaults to [0,0], which means that you won’t see the shadow unless it has a non-zero radius.
shadowPathif you set this property, Core Animation will use this path as a basis to compute the shadow instead of using the alpha channel of the layer. You can use this to generate a shadow that has nothing to do with the layer’s contents - the layer might even be empty.
Inner shadows are not very easy to implement.
I’ve searched for solutions to this for a while. Apparently, people on the interwebz are doing this by first creating a shape that is the negative of the shape you want to give a drop shadow to, then giving a drop shadow to this negative shape, and then masking the negative shape with the original shape, so that only its shadow remains. This works, but generating the negative shape is very hard. So I’ve developed my own solution - it’s similar but easier to implement, and it covers the vast majority of cases where you might want an inner shadow. It’s a three step process:
- Create a path by stroking the path of the shape you want to have an inner shadow, using
- Create a sublayer, set its shadow properties as desired, and set its shadowPath to the stroked path
- Mask the sublayer with the original shape
In other words, what we’re doing is making the contour of the shape drop a shadow, which of course will drop it inside and outside of the shape. But then we mask away the part of the shadow we don’t need, which is very easy as the mask coincides with the shape.
This said, I would be way happier if Core Animation supported inner shadows out of the box, like it does for drop shadows.
You can see this in action in the first screen of the attached example project, where you can also move your finger around to manipulate the shadowOffset property of some layers. Check out GBShadowsView.m in the sample project for the full code.
Some people have trouble implementing infinite rotation, which you might use for e.g. a custom spinner while waiting on network activity. Or to create an (astronomically inaccurate) space scene.
It’s actually quite simple.
CABasicAnimation* earthRotationAnimation; earthRotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; earthRotationAnimation.toValue = [NSNumber numberWithFloat:M_PI * 2.0]; earthRotationAnimation.duration = 10; earthRotationAnimation.repeatCount = INFINITY; [self.earthImageLayer addAnimation:earthRotationAnimation forKey:@"rotationAnimation"];
If your object isn’t rotating around its center, just create an empty layer or view where you want your object to be, and add your object in the middle of it. Core Animation is smart enough, so the performance hit when adding empty layers or views is negligible if not null.
In the sample app, the starry background is generated using a CAEmitterLayer, which is at the core of Core Animation’s particle system. You can create a lot of nice effects with particles, so check the code in GBRotationView.m if you’re interested.
A CAGradientLayer has two interesting properties, both animatable:
An array of
NSNumberobjects that define the gradient stops in unit coordinates, e.g.
@[@(0.0f), @(0.3f), @(0.9f), @(1.0f)]. If nil, Core Animation will assume you want uniformly spread stops.
An array of
CGColorRefobjects that specifies the color of each stop.
To create the animated “barcode scanner” thingy in the picture, I set my CAGradientLayer’s
colors to [transparent, red, transparent] and then I animated the
locations back and forth between [0.3, 0.35, 0.4] and [0.6, 0.65, 0.7], creating the illusion of a red laser moving up and down.
Doing this with a gradient is a terrible idea, but this might give you some insight into what can be accomplished with gradients alone. Can’t wait to see what crazy animated psychedelic patterns you can come up with :)
Fun with Masking
Did you know that if you give a shadow to a layer you’ll use as a mask, the shadow will be part of the mask? And that everything will still be animatable?
What if we used this to create a cool, animated “flashlight effect”? Check out the sample project to see how this is done - it’s very simple.
A Note About Performance
We used a bunch of shadows today. Keep in mind that computing the shadow from the layer’s alpha channel (which is the default behavior) is a very slow operation. You should always set
shadowPath, so Core Animation can skip that operation - this has a dramatic effect on performance.
At the top of GBMaskingView.m there’s a
SET_SHADOW_PATHS #define. Try setting that to
NO, so you can see the huge performance hit with your own eyes.
I plan to write an entire article about performance on iOS, which will include a Core Animation section, so stay tuned.
Download the sample project, hack away, and have fun. Tweet at me if you have questions, and I’ll make sure to update this article.
Planet images included with the sample project courtesy of some guy.