I need to animate different CATextLayers depending on some conditions. This is why I wrote a method that accepts the layer and animates it. Here it is:
- (void) animateText:(CATextLayer*) text withColor: (CGColorRef) color andDuration: (NSTimeInterval) duration
{
CABasicAnimation *colorAnimation = [CABasicAnimation
animationWithKeyPath:#"foregroundColor"];
colorAnimation.duration = duration;
colorAnimation.fillMode = kCAFillModeForwards;
colorAnimation.removedOnCompletion = NO;
colorAnimation.fromValue = (id)[UIColor redColor].CGColor;
colorAnimation.toValue = (id)[UIColor blackColor].CGColor;
colorAnimation.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionLinear];
[text addAnimation:colorAnimation
forKey:#"colorAnimation"];
"text" is a CATextLayer created, initialized and added to sublayers in some other method.
The problem is this code does not make the "text" layer animate.
(Please, don't pay attention that the method receives the color. I have temporarily put red and black for testing.)
I started testing. If I create a layer within this method and then try to animate this newly created method, it works ok.
If I copy-paste the code to the method where all layers are created and try to animate one of them - it works fine too.
But I need to animate different layers at different moments, so I see this function as essential.
Please, explain me what I am doing wrong.
Thank you in advance.
UPDATED:
Here's how the layers are created and placed onto UIView
//creating 110 text layers
CGFloat leftPoint = 0;
CGFloat topPoint = 0;
for (int i=0; i<11; ++i)
{
for (int j=0; j<10; ++j)
{
CATextLayer *label = [[CATextLayer alloc] init];
[label setString:[_labels objectAtIndex:j+(i*10)]]; // _labels contains letters to display
[label setFrame:CGRectMake(leftPoint, topPoint, _labelWidth, _labelHeight)]; // _labelWidth and _labelHeight are valid numbers
[label setAlignmentMode:kCAAlignmentCenter];
[label setForegroundColor:_textColor]; // _textColor is a CGColorRef type
[label setFont:(__bridge CFTypeRef)(_font.fontName)]; // font is UIFont type
[label setFontSize:[_font pointSize]];
[_textViews addObject:label]; // saving to internal array variable
[[self layer] addSublayer:label];
leftPoint += _labelWidth;
}
leftPoint = 0;
topPoint += _labelHeight;
}
If I place a code like this in the same method where I do animating, it works.
And it stops working when I pass the layer in the method like this:
for (int i = 0; i<labelsToHighlight.count; ++i) // labelsToHighlight is an array containing indexes of the CATextLayers which I need to animate
{
NSUInteger index = [[labelsToHighlight objectAtIndex:i] intValue];
[self animateText:[_textViews objectAtIndex:index] withColor: _highlightedTextColor andDuration:_redrawAnimationDuration];
}
It seems I managed to make it work:
Here's the correct method:
- (void) animateText:(CATextLayer*) text fromColor: (CGColorRef) fromColor toColor: (CGColorRef) toColor andDuration: (NSTimeInterval) duration
{
CABasicAnimation *colorAnimation = [CABasicAnimation
animationWithKeyPath:#"foregroundColor"];
colorAnimation.duration = duration;
colorAnimation.fillMode = kCAFillModeForwards;
colorAnimation.removedOnCompletion = NO;
colorAnimation.fromValue = (__bridge id)fromColor;
colorAnimation.toValue = (__bridge id) toColor;
colorAnimation.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionLinear];
[text setForegroundColor:toColor]; // This is what I have actually added
[text addAnimation:colorAnimation forKey:#"colorAnimation"];
}
But to be fully honest, I don't understand why that line did make it work.
I suppose that without resetting foreground color the animation should have blinked and foreground color would return to its initial value. In reality though absolutely nothing happened on the screen.
After adding this everything works just fine.
Related
I want to animate the UIView default background layer. In viewDidLoad I'm adding my custom property to animate to the view with:
self.layer.delegate = self;
[self.layer setValue:0 forKey:#"value"];
as described here.
The setter method of the value to change looks like this and updates the added custom property on the background layer:
- (void)setValue:(NSInteger)value
{
_value = value;
[self.layer setValue:[NSNumber numberWithInteger:value] forKey:#"value"];
[self setNeedsDisplay];
}
And in actionForLayer:forKey: overwritten in my view I'm returning an animation, as described here, which should animate from the current value to the new set value like this:
- (id<CAAction>)actionForLayer:(CALayer*)layer forKey:(NSString*)key
{
if([key isEqualToString:#"value"])
{
CAAnimation* action = (CAAnimation*)[self actionForLayer:layer forKey:#"backgroundColor"];
if(action != (CAAnimation*)[NSNull null])
{
CABasicAnimation* animation = [CABasicAnimation animation];
animation.keyPath = #"value";
animation.fromValue = [self.layer valueForKey:#"value"];
animation.toValue = [NSNumber numberWithInteger:_value];
animation.beginTime = action.beginTime;
animation.duration = action.duration;
animation.speed = action.speed;
animation.timeOffset = action.timeOffset;
animation.repeatCount = action.repeatCount;
animation.repeatDuration = action.repeatDuration;
animation.autoreverses = action.autoreverses;
animation.fillMode = action.fillMode;
animation.timingFunction = action.timingFunction;
animation.delegate = action.delegate;
[self.layer addAnimation:animation forKey:#"value"];
}
}
return [super actionForLayer:layer forKey:key];
}
And I draw my view content like this:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, rect);
[self drawARainbowUnicorn:context];;
}
From my view controller I do an block animation like this:
[UIView animateWithDuration:10.0 animations:
^{
self->_myView.value = self->_myView.maximumValue;
}];
What I see is that the value always appears as animated on the final position. What I would expect is that the value would animate to that position in the given time.
All the examples I found add a separate layer to animate like this
or this
or this and I'm aware of this StackOverflow question and some others I found.
I also had a look at this older sample project from Apple. They also create a CALayer subclass. But why is that needed?
I don't understand why the default background layer shouldn't do it as well. What do I miss here?
I can draw points with code below, but I need to draw every point one by one with animation. When I try this, all points appear on the screen only after the for loop ends.
I did some research but I could not figure out why these points are not drawing one by one in the for loop. singerData is an array which has x values of points.
-(void)drawDotGraphForUser
{
UIView *viewAnimationUser = [[UIView alloc]init];
viewAnimationUser.frame = CGRectMake(0, 0, 320, 100);
for (int i = 0; i<singerPitchDataSize; i++)
{
CALayer *layer = [CALayer layer];
NSAssert1(layer != nil, #"failed to create CALayer #%i", i);
layer.backgroundColor = [UIColor colorWithRed:0 green:0 blue:1 alpha:1.0].CGColor;
layer.frame = CGRectMake(i+1, singerData[i], 1.0, 1.0);
NSLog(#"GraphUser= %d=%f",i,singerData[i]);
NSLog(#"MSprites=%#",mSprites[i]);
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 5;
pathAnimation.fromValue = [NSNumber numberWithInt:0];
pathAnimation.toValue = [NSNumber numberWithInt:singerPitchDataSize];
[layer addAnimation:pathAnimation forKey:#"strokeEnd"];
[viewAnimationUser.layer addSublayer:layer];
[self.view addSubview:viewAnimationUser];
}
}
These are all appearing to be happening at the same time because your animations are being sent to another thread automatically extremely quickly. If you want them to appear one by one you will need to send them to add them to the subview slower by adding a timer at the end of your function or you will need to listen for the animation to end to trigger the next one.
I am trying to perform a transform for the views on y axis using CABasicAnimation:
for(int x = 0; x < viewsArray.count; x++)
{
CABasicAnimation *startAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
[startAnimation setToValue:[NSNumber numberWithFloat:DEGREES_RADIANS(-55.0f)]];
[startAnimation setDuration:5.0];
startAnimation.delegate = self;
UIView *view = [viewsArray objectAtIndex:x];
[startAnimation setValue:#"rotate" forKey:#"id"];
[view.layer addAnimation:startAnimation forKey:#"rotate"];
}
Nothing fancy. I would like just to keep the modal and the animation in sync, so in
animationDidStop I try to set the transform again using a for-loop:
if([[anim valueForKey:#"id"] isEqualToString:#"rotate"])
{
aTransform = CATransform3DRotate(aTransform, DEGREES_RADIANS(-55.0), 0.0, 1.0, 0.0);
for(int x = 0; x < viewsArray.count; x++)
{
UIView *view = [viewsArray objectAtIndex:x];
view.layer.transform = aTransform;
}
}
But I realized after the animation stops the animation would jerk to the angle set by the transform in animationDidStop.
Does any one know why and what would be the best way to do this without having to use removedOnCompletion = NO;? I would like to avoid using that and would like to keep the animation always in sync with the modal layer.
You can set it at the same time as you add your animation, but you'll need to supply a fromValue to the animation to stop it updating the presentation layer immediately:
for(int x = 0; x < viewsArray.count; x++)
{
UIView *view = [viewsArray objectAtIndex:x];
NSString *keyPath = #"transform.rotation.y";
NSNumber *toValue = [NSNumber numberWithFloat:DEGREES_RADIANS(-55.0f)];
CABasicAnimation *startAnimation = [CABasicAnimation animationWithKeyPath:keyPath];
[startAnimation setFromValue:[view.layer valueForKeyPath:keyPath]];
//[startAnimation setToValue:toValue]; We don't need to set this as we're updating the current value on the layer instead.
[startAnimation setDuration:5.0];
[view.layer addAnimation:startAnimation forKey:#"rotate"];
[view.layer setValue:toValue forKeyPath:keyPath]; // Update the modal
}
I've made a custom class of CAGradientLayer (added radial gradient option).
I works but I can't figure out why I can't animate the colors.
Here's my code.
The animation is a simple CABasicAnimation where I set the toValue to a new array with colors.
#import "ExtCAGradientLayer.h"
NSString * const kExt_CAGradientLayerRadial = #"RadialGradient";
#implementation ExtCAGradientLayer
#synthesize gradientDrawingOptions;
+(BOOL)needsDisplayForKey:(NSString *)key
{
return [key isEqualToString:#"type"] || [super needsDisplayForKey:key];
}
- (void)drawInContext:(CGContextRef)theContext
{
if ([self.type isEqualToString:kExt_CAGradientLayerRadial]) {
size_t num_locations = self.locations.count;
int numbOfComponents = 0;
CGColorSpaceRef colorSpace = NULL;
if (self.colors.count) {
CGColorRef colorRef = (__bridge CGColorRef)[self.colors objectAtIndex:0];
numbOfComponents = CGColorGetNumberOfComponents(colorRef);
colorSpace = CGColorGetColorSpace(colorRef);
}
float *locations = calloc(num_locations, sizeof(float));
float *components = calloc(num_locations, numbOfComponents * sizeof(float));
for (int x = 0; x < num_locations; x++) {
locations[x] = [[self.locations objectAtIndex:x] floatValue];
const CGFloat *comps = CGColorGetComponents((__bridge CGColorRef)[self.colors objectAtIndex:x]);
for (int y = 0; y < numbOfComponents; y++) {
int shift = numbOfComponents * x;
components[shift + y] = comps[y];
}
}
CGPoint position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGFloat radius = floorf(MIN(self.bounds.size.width, self.bounds.size.height) / 2);
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, components, locations, num_locations);
CGContextDrawRadialGradient(theContext, gradient, position, 0, position, radius, self.gradientDrawingOptions);
free(locations);
free(components);
}
Layers have a bunch of implicit animations tied to them. This can make them a little funny when interacting with CABasicAnimation. My first recommendation is to avoid CABasicAnimation when working with layers. There's almost never a good reason for it, since layers know how to animate themselves. It's much easier just to do this:
[CATransaction begin];
[CATransaction setAnimationDuration:3];
layer.colors = [NSArray arrayWithObjects:
(__bridge id)[UIColor redColor].CGColor,
[UIColor greenColor].CGColor,
nil];
[CATransaction commit];
That's a 3-second animation of the colors. Easy. The begin/commit aren't technically needed in most cases, but they limit the scope of the setAnimationDuration: in case you're animating other things in this transaction.
But maybe you like CABasicAnimation, or you're using it something that returns them (I do that sometimes). You can still combine them with CALayer, but you have to tell CALayer to stop its automatic animation (setDisableActions:). And you still want to use the layer setters rather than toValue:.
[CATransaction begin];
[CATransaction setDisableActions:YES];
CABasicAnimation *anim = [[CABasicAnimation alloc] init];
anim.duration = 3;
[layer addAnimation:anim forKey:#"colors"];
layer.colors = [NSArray arrayWithObjects:
(__bridge id)[UIColor redColor].CGColor,
[UIColor greenColor].CGColor,
nil];
[CATransaction commit];
If you use toValue: rather than the setter, it will work, but will "jump back" to the original value at the end.
Avoid solutions that suggest setting fillMode and removedOnCompletion. These generate many subtle problems if you don't understand exactly what they're doing. They seldom are what you really mean; they just happen to work as long as things are very simple.
I am a bit of a beginner with animations and have been experimenting with CoreAnimation for a couple of days. Feel free to warn me if this question does not make sense, but I'm trying to achieve the following. I have three objects:
one should be an image, moving according to a given pattern
one should be an UIImage that swaps two images
one should be a text (CATextLayer?) whose content changes
The three actions should happen in sync.
As an example, think about a programme showing a sinusoid function, like in a ECG, oscillating between -1 and +1: the first image would then move according to the current value (-1, -0.9, -0.8, ... 0, +0.1, +0.2, ... 1), the swap image would show "+" for positive values and "-" for negative values, and the text would alternate between "Positive" and "Negative".
I've tried with CAAnimationGroup but I'm clearly missing something. Some code:
{
// image that moves
CALayer *img1 = [CALayer layer];
img1.bounds = CGRectMake(0, 0, 20, 20);
img1.position = CGPointMake(x1,y1);
UIImage *img1Image = [UIImage imageNamed:#"image.png"];
img1.contents = (id)img1Image.CGImage;
// image that changes
CALayer *swap = [CALayer layer];
swap.bounds = CGRectMake(0, 0, 30, 30);
swap.position = CGPointMake(x2,y2);
NSString* nameswap = #"img_swap_1.png"
UIImage *swapImg = [UIImage imageNamed:nameswap];
// text
CATextLayer *text = [CATextLayer layer];
text.bounds = CGRectMake(0, 0, 100, 100);
text.position = CGPointMake(x3,y3);
text.string = #"Text";
// create animations
CGFloat duration = 0.2;
CGFloat totalDuration = 0.0;
CGFloat start = 0;
NSMutableArray* animarray = [[NSMutableArray alloc] init];
NSMutableArray* swapanimarray = [[NSMutableArray alloc] init];
NSMutableArray* textanimarray = [[NSMutableArray alloc] init];
float prev_x = 0;
float prev_y = 0;
// I get my values for moving the object
for (NSDictionary* event in self.events) {
float actual_x = [[event valueForKey:#"x"] floatValue];
float actual_y = [[event valueForKey:#"y"] floatValue];
// image move animation
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position"];
CGPoint startPt = CGPointMake(prev_x,prev_y);
CGPoint endPt = CGPointMake(actual_x, actual_y);
anim.duration = duration;
anim.fromValue = [NSValue valueWithCGPoint:startPt];
anim.toValue = [NSValue valueWithCGPoint:endPt];
anim.beginTime = start;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[animarray addObject:anim];
// image swap animation
CABasicAnimation *swapanim = [CABasicAnimation animationWithKeyPath:#"contents"];
swapanim.duration = duration;
swapanim.beginTime = start;
NSString* swapnamefrom = [NSString stringWithFormat:#"%#.png", prev_name];
NSString* swapnameto = [NSString stringWithFormat:#"%#.png", current_name];
UIImage *swapFromImage = [UIImage imageNamed:swapnamefrom];
UIImage *swapToImage = [UIImage imageNamed:swapnameto];
swapanim.fromValue = (id)(swapFromImage.CGImage);
swapanim.toValue = (id)(swapToImage.CGImage);
swapanim.fillMode = kCAFillModeForwards;
swapanim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[swapanimarray addObject:swapanim];
//text animation
CABasicAnimation *textanim = [CABasicAnimation animationWithKeyPath:#"contents"];
textanim.duration = duration;
textanim.fromValue = #"Hey";
textanim.toValue = #"Hello";
textanim.beginTime = start;
[textanimarray addObject:textanim];
// final time settings
prev_x = actual_x;
prev_y = actual_y;
start = start + duration;
totalDuration = start + duration;
}
}
CAAnimationGroup* group = [CAAnimationGroup animation];
[group setDuration:totalDuration];
group.removedOnCompletion = NO;
group.fillMode = kCAFillModeForwards;
[group setAnimations:animarray];
CAAnimationGroup* swapgroup = [CAAnimationGroup animation];
[swapgroup setDuration:totalDuration];
swapgroup.removedOnCompletion = NO;
swapgroup.fillMode = kCAFillModeForwards;
[swapgroup setAnimations:swapanimarray];
CAAnimationGroup* textgroup = [CAAnimationGroup animation];
[textgroup setDuration:totalDuration];
textgroup.removedOnCompletion = NO;
textgroup.fillMode = kCAFillModeForwards;
[textgroup setAnimations:textanimarray];
[ball addAnimation:group forKey:#"position"];
[swap addAnimation:flaggroup forKey:#"position"];
[text addAnimation:textgroup forKey:#"contents"];
[self.layer addSublayer:ball];
[self.layer addSublayer:swap];
[self.layer addSublayer:text];
}
Now... the problems:
1) the swap image reverts to the original at every swap. So, if I swap A->B, I see it going from A to B in the expected duration time, but then reverting to A. I've read a number of threads on SO about this but couldn't get it to work.
2) changing the string of the text layer in a timed fashion... is this possible with this infrastructure? Basically, I'm trying to get the text and the swap image to change as soon as the first image moves, as described in the example.
3) setting the delegate for the CABasicAnimation doesn't have any effect, although it does for the CAAnimationGroup: as a result, you can't manage events like animationDidStop for every single animation, just for the whole group. Is there any alternative way to do so?
4) following from 3), is it possible, using CAAnimationGroup, to intercept the events to create a stop/start behaviour? Let's suppose I wanted to have play/stop buttons, and to resume the animation from exactly where I had left it?
As a conclusive question, I would simply like to know if anyone did something similar and, most importantly, if this way of doing things (using a CAAnimationGroup) is actually the way to go or if it's better to use CAKeyFrameAnimation or something else.
I managed to solve the problem, albeit with some workarounds.
1) The trick here is that
removedOnCompletion = NO;
fillMode = kCAFillModeForwards;
need to be assigned to every single animation, not to the CAAnimationGroup.
2) This requires a separate animation to be determined for the CATextLayer, and animating the "string" property. In this case, the code was right except for the "contents" key-value which should have been
[text addAnimation:textgroup forKey:#"contentAnimate"];
Alternatively, see point 3. The callback way of operation allows to change a label.text.
3) The only way to do this is to actually not use the CAAnimationGroup and set up a sequence of CABasicAnimation. The general schema is: create the first CABasicAnimation in a function, assign the delegate. In the animationDidStop method, add a callback to the function that creates the CABasicAnimation. This way, each animation will be created. At the same time, this allows to intercept and react to specific events within the animation.
-(void)performNextAnimation:(int)index {
// ... this gives for granted you have an object for #index ...
NSDictionary* thisEvent = [self.events objectAtIndex:index];
float prev_x = [thisEvent valueForKey:#"prev_x"];
float prev_y = [thisEvent valueForKey:#"prev_x"];
float actual_x = [thisEvent valueForKey:#"prev_x"];
float actual_y = [thisEvent valueForKey:#"prev_x"];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position"];
GPoint startPt = CGPointMake(prev_x,prev_y);
CGPoint endPt = CGPointMake(actual_x, actual_y);
anim.duration = 0.5;
anim.fromValue = [NSValue valueWithCGPoint:startPt];
anim.toValue = [NSValue valueWithCGPoint:endPt];
anim.beginTime = start;
[anim setDelegate:self];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[object addAnimation:anim forKey:#"position"];
}
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
// ... your conditions go here ...
// ... your actions go here ...
// e.g. set label.text
[self performNextAnimation];
}
4) Following from 3, this is not possible in CAAnimationGroup. Reading the docs, I would say that CAAnimationGroup is not intended to be used for sequences in which each event represents a step forward in time (for this you need to use CAKeyFrameAnimation or a sequence of CABasicAnimation with a callback function, as I did). CAAnimationGroup is intended for linked animation that should be executed on an all-or-nothing basis (similarly to CATransaction).