I have been struggling with a Core Animation problem for over two weeks. I am going to vent here, because, frankly, I am not sure what else to do.
I have subclassed CALayer
to create a layer called LetterLayer
. The purpose of this layer is to display a single letter with a given font and text color[1]. I have the text display mostly worked out, but I am having trouble with animating the text color. Here is a quick recap of what I have tried, what has not worked and what I plan to do next.
The LetterLayer subclass adds three properties, letter
, foregroundColor
and font
. I originally wrote accessor and setter methods for each of these properties by hand. I have still not gotten used to the Objective-C 2.0 features, yet.
The LetterLayer
class renders its content into the CGContextRef
that is passed into -(void)drawInContext:(CGContextRef)context;
. At that time it takes the values specified for letter
, foregroundColor
and font
and renders a single letter within the bounds of the clipping rectangle that is set on the given graphics context.
All of that works great. Now on to the animation troubles.
I am animating the bounds of the layers after they are displayed. I have a CAKeyframeAnimation that makes the layers grow from a size [1, 1]
to a size of [layer.width, layer.height]
. I create the CAKeyframeAnimation
instance and set its values property to a 2 element array containing the initial size and the destination sizes for the bounds
property. Here is the code that I am using.
// grow animation
CAKeyframeAnimation* scaleUpAnimation =
[CAKeyframeAnimation animationWithKeyPath: @"bounds"];
scaleUpAnimation.fillMode = kCAFillModeForwards;
scaleUpAnimation.removedOnCompletion = NO;
scaleUpAnimation.timeOffset = 3000;
scaleUpAnimation.speed = 0.1;
scaleUpAnimation.values = [NSArray arrayWithObjects:
[NSValue valueWithRect: initialRect],
[NSValue valueWithRect: destinationRect],
nil];
[letterLayer addAnimation: scaleUpAnimation
forKey: @"frameSizeAnimation"];
Okay. So that works. The text grows very nicely and slowly, just like I want it to. So, now on to the trouble that I am having.
I cannot for the life of me seem to be able to animate any changes to foregroundColor
. Okay. I should correct that statement. I have not been able to explicitly animate foregroundColor
. When I assign a new value to foregroundColor, the changes display just fine. And from what I can tell, the change takes place smoothly. I should note that this will not happen if the setter method for foregroundColor
does not have a call to -(void)setNeedsDisplay;
.
I have tried just about everything that I can think of to make the color change animation take place using a CAKeyframeAnimation
. I have also tried a CABasicAnimation
, and it does not work either.
Last night, in desperation, I decided to instrument the calls to + (id) defaultValueForKey: (NSString*) keyPath;
, -(id<CAAction>)actionForKey: (NSString*) key;
and -(CAAnimation*)animationForKey:(NSString*)key;
. I basically adding a call to NSLog
so that I could see what key path values each of these methods were getting called for. And you know what I discovered? None of these methods were getting called with a key path of "foregroundColor"
.
So with this discovery in hand, I decided to change the declaration of foregroundColor
and the other properties, so that they are defined with the @property
attribute. That had an effect. Now +(id)defaultValueForKey:(NSString*)keyPath;
and -(id<CAAction>)actionForKey:(NSString*)key;
were getting called. But the animation still does not work.
The next thing that I tried was to have -(id<CAAction>) actionForKey:(NSString*)key;
return a CAKeyframeAnimation
instance. That did not work. And neither did returning a CABasicAnimation
instance.
Okay. The next thing that I tried. I had read somewhere, although I cannot remember where, that the CALayer
class provides a default implementation for properties that do not have accessor and setter methods. So I commented out the accessor and setter methods that I had written. Well that did have an effect, but not the one that I wanted. Now -(void)setNeedsDisplay;
is not called whenever foregroundColor
is modified, so the letter only shows with the color that it is created with. Wow. Now I am moving backwards.
At this point, I am not sure what to do next. I seem to have have missed something fundamental, but I am not sure what. I have poured over the documentation in search of my mistake, and I have yet to find anything. I have searched example code. I have trolled blogs about Core Animation. I have checked all of the Core Animation articles at CocoaDev. I have bought and downloaded the new book on Core Animation that is currently in beta. I have tried everything that I can think of. Yet, I am left with nothing.
I have started to wonder if the issue is with CGColorRef
properties in general. So, the next thing that I am going to try is to write a simple test CALayer
subclass that provides a custom property. I am going to chose a type of something other than CGColorRef
, perhaps int
and see if I have any better success with that. I am also going to see if I can provide an explicit animation for backgroundColor
.
I will post whatever I find out, but if anyone out there reads this and thinks that they might have an answer, then please, please post a comment or send me an email message.
Help me vast interweb! You are my only hope.
[1]: For those of you that may want to point out that I should be using a CATextLayer
for this, I say bah! I have already tried that. I need to make sure that the layer size is the exact bounds of the character that is rendered into it. CATextLayer
uses Mac OS X’s text rendering engine to ensure that are characters that are rendered into a layer are displayed with all of their baseline’s aligned. CATextLayer
also positions characters so that taller characters can be rendered next to short characters without getting clipped. And it also make sure that characters that extend below the baseline are not clipped. This is great when you are trying to render whole words, but it causes a bunch of problems when you are trying to render a single character.
Update: I have provided more information in another post.