http://www.nomadplanet.fr/2010/11/animate-calayer-custom-properties-with-coreanimation/
Apple’s Core Animation framework is a powerful way for developpers to produce animated content in their graphical interfaces. It is simple enough so that the developer only needs to specify the initial and final states of an object in order to make it animate. Core Animation handles interpolation of intermediate values and executes the animation in different thread than the main run loop, the developper doesn’t need to write specific code for the animations. And CoreAnimations can be automagically accelerated by the GPU.
CoreAnimation can be used in several ways, from the high-level UIView animation syntax down to implicit and explicit CAAnimations.
All animations finally end up being animations of CALayers properties. There are a number of CALayer properties that can be animated, they are called the animatable-properties by Apple (opacity, bounds, content, cornerRadius etc…)
However in some cases, the developper may need to implement its own CALayer subclass. The main purpose of doing such a thing is to let the developer draw custom content in the drawInContext: method (a bit like the drawRect: UIView method).
Let’s say you have a custom CALayer, which basically draws a circle with a given radius. The circle have a border on which you can control the thickness. Let’s name that class CircleLayer.
Basically its interface would look like this:
1 | #import <Foundation/Foundation.h> |
2 | #import <QuartzCore/QuartzCore.h> |
4 | @interface CircleLayer : CALayer { |
9 | @property ( nonatomic , assign) CGFloat radius; |
10 | @property ( nonatomic , assign) CGFloat strokeWidth; |
and in CircleLayer.m
1 | - ( void )drawInContext:(CGContextRef)ctx { |
2 | NSLog ( @"Drawing layer, strokeWidth is %f, radius is %f" , self .strokeWidth, self .radius); |
4 | CGPoint centerPoint = CGPointMake(CGRectGetWidth( self .bounds)/2, CGRectGetHeight( self .bounds)/2); |
7 | CGContextAddArc(ctx, centerPoint.x, centerPoint.y, self .radius, 0.0, 2*M_PI, 0); |
8 | CGContextClosePath(ctx); |
11 | CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor); |
12 | CGContextFillPath(ctx); |
15 | CGContextAddArc(ctx, centerPoint.x, centerPoint.y, self .radius, 0.0, 2*M_PI, 0); |
16 | CGContextClosePath(ctx); |
19 | CGContextSetStrokeColorWithColor(ctx, [UIColor lightGrayColor].CGColor); |
20 | CGContextSetLineWidth(ctx, self .strokeWidth); |
21 | CGContextStrokePath(ctx); |
If the developper want to make the radius and/or the stroke width of the circle to animate, he would implement a CAAnimation with a fromValue and toValue:
1 | - ( void ) animateRadius { |
2 | CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath: @"radius" ]; |
4 | anim.fromValue = [ NSNumber numberWithDouble:50.0]; |
5 | anim.toValue = [ NSNumber numberWithDouble:150.0]; |
6 | anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
8 | [circleLayer addAnimation:anim forKey: @"animateRadius" ]; |
10 | circleLayer.radius = 150.0; |
13 | - ( void ) animateStrokeWidth { |
14 | CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath: @"strokeWidth" ]; |
16 | anim.fromValue = [ NSNumber numberWithDouble:25.0]; |
17 | anim.toValue = [ NSNumber numberWithDouble:1.0]; |
18 | anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; |
20 | [circleLayer addAnimation:anim forKey: @"animateStrokeWidth" ]; |
22 | circleLayer.strokeWidth = 1.0; |
However these two methods (which would be placed for instance in the UIView hosting the circleLayer) attempt to animate radius and strokeWidth which are not part of CoreAnimation’s animatable properties. So they wont animate.
But there is a solution, which is not really clear to me in Apple’s documentation. CircleLayer.m needs to implement two other methods:
The first, needsDisplayForKey: tells CoreAnimation which properties of the layer causes the layer to be marked as ‘dirty’ (i.e: needs to be redrawn by drawInContext: method).
1 | + ( BOOL )needsDisplayForKey:( NSString *)key { |
2 | if ([key isEqualToString: @"radius" ] |
3 | || [key isEqualToString: @"strokeWidth" ]) { |
7 | return [ super needsDisplayForKey:key]; |
So here we tell that an update on radius or on strokeWidth should cause the layer to be redrawn (other properties cause the redraw too, with the help of [super needsDisplayForKey:key]).
The second, initWithLayer: will ensure that custom properties will be copied for presentation layers. When you animate a CALayer, it creates presentation copies of the layer (using initWithLayer:) for each and every frame of the animation. Each presentation layer contains an intermediate value of the animated properties. The original layer contains the final state of the properties.
1 | - ( id ) initWithLayer:( id )layer { |
2 | if ( self = [ super initWithLayer:layer]) { |
3 | if ([layer isKindOfClass:[CircleLayer class ]]) { |
4 | CircleLayer *other = (CircleLayer*)layer; |
5 | self .radius = other.radius; |
6 | self .strokeWidth = other.strokeWidth; |
Since our custom properties are not copied automatically by CALayer, not implementing initWithLayer: would result inradius and strokeWidth to be zeroed for each intermediate frame, and then finally jumping to their last value when the last frame is displayed.
The radius and border thickness currently being animated
Now you have a simple solution to animate whatever kind of properties on your custom CALayer as long as they represent numbers (which can be interpolated). And yes it works with NSNumbers too.
[UPDATE]: You can find a demo project in this article.