Synchronising image, text, and positioning with CoreAnimation - ios

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).

Related

How to remove strange delay between two sequential CAKeyframeAnimation animations?

iOs-coders!
This code to animate the little red square to drawing big sign "8" on the UIView. First an upper ring (animSequence1 = 5 sec), then right away a lower ring (animSequence2 = another 5 sec).
No delay needed!
But I stucked on strange delay (about 1 sec) between two sequential animations. Whence did this delay come from?!?
Code:
- (void)drawRect:(CGRect)drawRect {
CGContextRef context = UIGraphicsGetCurrentContext();
drawRect = CGRectMake(0, 0, 320, 320);
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor;);
CGContextFillRect(context, drawRect);
// Little red square
CALayer *layerTest;
layerTest = [CALayer layer];
layerTest.bounds = CGRectMake(0, 0, 20, 20);
layerTest.anchorPoint = CGPointMake(0, 0);
layerTest.backgroundColor = [UIColor redColor].CGColor;
[[self layer] addSublayer:layerTest];
// Upper ring
CAKeyframeAnimation *animSequence1;
animSequence1 = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animSequence1.fillMode = kCAFillModeForwards;
animSequence1.removedOnCompletion = NO;
animSequence1.autoreverses = NO;
animSequence1.repeatCount = 0;
animSequence1.duration = 5.0;
animSequence1.beginTime = 0.0;
animSequence1.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 120) radius:80 startAngle:DEG_TO_RAD(90) endAngle:DEG_TO_RAD(450) clockwise:YES].CGPath;
animSequence1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Lower ring
CAKeyframeAnimation *animSequence2;
animSequence2 = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animSequence2.fillMode = kCAFillModeForwards;
animSequence2.removedOnCompletion = NO;
animSequence2.autoreverses = NO;
animSequence2.repeatCount = 0;
animSequence2.duration = 5.0;
animSequence2.beginTime = 5.0;
animSequence2.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 280) radius:80 startAngle:DEG_TO_RAD(-90) endAngle:DEG_TO_RAD(-450) clockwise:NO].CGPath;
animSequence2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// A sequence of animations
CAAnimationGroup *animGroup;
layerTest.position = CGPointMake(200, 200);
animGroup = [CAAnimationGroup animation];
animGroup.duration = 10.0;
animGroup.animations = [NSArray arrayWithObjects:animSequence1, animSequence2, nil];
[layerTest addAnimation:animGroup forKey:nil];
}
Also I tried make it without CAAnimationGroup.
Set up [animSequence1 setDelegate:self]; for first CAKeyframeAnimation (animSequence1), and then start second CAKeyframeAnimation (animSequence2) by (void)animationDidStop.
It worked same, but this strange delay (about 1 sec) between two animations no dissapear!
QUESTION: How to remove strange delay between two sequential CAKeyframeAnimation animations? No delay needed!
Don’t create layers or animations in -drawRect:—you don’t have a whole lot of control over when it gets called, so you’re going to end up with multiple layerTests added to your view, all trying to animate separately. Create the layer / animation in an initializer, or in response to some user interaction.
I would also advise getting rid of your fillMode and removedOnCompletion settings on animSequence1; once animSequence2 starts, you’re effectively asking CA to run two non-additive animations once on the same property. Not sure whether the behavior for that is well-defined, but it’s another place the weirdness could be coming from.

CAAnimationGroup Running Too Fast

My CAAnimationGroup is running too fast. I want it to take a total of 50 seconds to do something, and it is doing it in about 3...it breezes through the first animation, and does the other one slow.
hover = [CABasicAnimation animationWithKeyPath:#"position"];
hover.fillMode = kCAFillModeForwards;
hover.removedOnCompletion = NO;
hover.additive = YES; // fromValue and toValue will be relative instead of absolute values
hover.fromValue = [NSValue valueWithCGPoint:CGPointZero];
hover.toValue = [NSValue valueWithCGPoint:CGPointMake(26*22, 26*-10.0)]; // y increases downwards on iOS
hover.autoreverses = FALSE; // Animate back to normal afterwards
hover.repeatCount = 0; // The number of times the animation should repeat
CABasicAnimation *fall = [CABasicAnimation animationWithKeyPath:#"position"];
fall.fillMode = kCAFillModeForwards;
fall.removedOnCompletion = NO;
fall.additive = YES; // fromValue and toValue will be relative instead of absolute values
fall.fromValue = [NSValue valueWithCGPoint:CGPointMake(26*22, 26*-10.0)];
fall.toValue = [NSValue valueWithCGPoint:CGPointMake(26*22, guess1*700.0)]; // y increases downwards on iOS
fall.autoreverses = FALSE; // Animate back to normal afterwards
fall.repeatCount = 0; // The number of times the animation should repeat
CAAnimationGroup* group = [CAAnimationGroup new];
group.beginTime = 0.;
[group setDuration:50.0];
group.animations = #[ hover, fall ];
[theDude.layer addAnimation:group forKey:#"myHoverAnimation"];
Judging by your implementation, I'm assuming you want a sequence of animations on an object theDude on it's position property.
You can achieve the animation by using CATransaction using it's completion blocks. More can be read here.

What is the maximum duration value (CFTimeInterval) for a CAAnimationGroup?

I have two rotation animations in my CAAnimationGroup, one that starts from zero and another that repeats and autoreverses from that state:
- (void)addWobbleAnimationToView:(UIView *)view amount:(float)amount speed:(float)speed
{
NSMutableArray *anims = [NSMutableArray array];
// initial wobble
CABasicAnimation *startWobble = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
startWobble.toValue = [NSNumber numberWithFloat:-amount];
startWobble.duration = speed/2.0;
startWobble.beginTime = 0;
startWobble.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[anims addObject:startWobble];
// rest of wobble
CABasicAnimation *wobbleAnim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
wobbleAnim.fromValue = [NSNumber numberWithFloat:-amount];
wobbleAnim.toValue = [NSNumber numberWithFloat:amount];
wobbleAnim.duration = speed;
wobbleAnim.beginTime = speed/2.0;
wobbleAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
wobbleAnim.autoreverses = YES;
wobbleAnim.repeatCount = INFINITY;
[anims addObject:wobbleAnim];
CAAnimationGroup *wobbleGroup = [CAAnimationGroup animation];
wobbleGroup.duration = DBL_MAX; // this stops it from working
wobbleGroup.animations = anims;
[view.layer addAnimation:wobbleGroup forKey:#"wobble"];
}
Since CFTimeInterval is defined as a double, I try setting the duration of the animation group to DBL_MAX, but that stops the animation group from running. However, If I set it to a large number, such as 10000, it runs fine. What is the largest number I can use for a duration of a CAAnimationGroup, to ensure it runs for as near to infinity as possible?
UPDATE: It appears that if I put in a very large value such as DBL_MAX / 4.0 then it freezes for a second, then starts animating. If I put in the value DBL_MAX / 20.0 then the freeze at the beginning is a lot smaller. It seems that having such a large value for the duration is causing it to freeze up. Is there a better way of doing this other than using a very large value for the duration?
I am faced with the exact same issue right now... I hope someone proves me wrong, but the only reasonable way I see to handle this situation is by moving the first animation to a CATransaction, and chaining that with the autoreverse animation using:
[CATransaction setCompletionBlock:block];
It's not ideal, but gets the job done.
Regarding your question about the animations being paused when coming back from background, that's a classic limitation of the CoreAnimation framework, many solutions have been proposed for it. The way I solve it is by simply reseting the animations at a random point of the animation, by randomizing the timeOffset property. The user can't tell exactly what the animation state should be, since the app was in the background. Here is some code that could help (look for the //RANDOMIZE!! comment):
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(startAnimating)
name:UIApplicationWillEnterForegroundNotification
object:[UIApplication sharedApplication]];
...
for (CALayer* layer in _layers)
{
// RANDOMIZE!!!
int index = arc4random()%[levels count];
int level = ...;
CGFloat xOffset = ...;
layer.position = CGPointMake(xOffset, self.bounds.size.height/5.0f + yOffset * level);
CGFloat speed = (1.5f + (arc4random() % 40)/10.f);
CGFloat duration = (int)((self.bounds.size.width - xOffset)/speed);
NSString* keyPath = #"position.x";
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:keyPath];
anim.fromValue = #(xOffset);
anim.toValue = #(self.bounds.size.width);
anim.duration = duration;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// RANDOMIZE!!
anim.timeOffset = arc4random() % (int) duration;
anim.repeatCount = INFINITY;
[layer removeAnimationForKey:keyPath];
[layer addAnimation:anim forKey:keyPath];
[_animatingLayers addObject:layer];
}
It is much simpler to use a single keyframe animation instead of a group of two separate animations.
- (void)addWobbleAnimationToView:(UIView *)view amount:(CGFloat)amount speed:(NSTimeInterval)speed {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = 2 * speed;
animation.values = #[ #0.0f, #(-amount), #0.0f, #(amount), #0.0f ];
animation.keyTimes = #[ #0.0, #0.25, #0.5, #0.75, #1.0 ];
CAMediaTimingFunction *easeOut =[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
CAMediaTimingFunction *easeIn =[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
animation.timingFunctions = #[ easeOut, easeIn, easeOut, easeIn ];
animation.repeatCount = HUGE_VALF;
[view.layer addAnimation:animation forKey:animation.keyPath];
}

Loop animation - doesn't go

I'm trying to repeat a sequence of animations, inside a LOOP, changing at each loop some parameters randomly. Here is the code. Anyone please knows why it doesn't work?
If I call it once with a button action, it works, but with a loop it doesn't.
Thanks a lot! Giuseppe
-(IBAction)startLoop:(id)sender {
for (int i=1;i<10; i++) {
[self animation2];
}
}
-(id) animation2 {
int max=500;
UIImage *myImage = [UIImage imageNamed:#"coccinella2.png"];
CALayer *myLayer = [CALayer layer];
myLayer.contents = (id)myImage.CGImage;
myLayer.bounds = CGRectMake(0, 0, 50, 60);
[myLayer setPosition:CGPointMake(arc4random()%(max), arc4random()%(max))];
[myLayer setBounds:CGRectMake(0.0, 0.0, 50.0, 60.0)];
[self.view.layer addSublayer:myLayer];
//translation1
CGPoint startPt = CGPointMake(arc4random()%(max),arc4random()%(max));
CGPoint endPt = CGPointMake(arc4random()%(max),arc4random()%(max));
CABasicAnimation *transl1 = [CABasicAnimation animationWithKeyPath:#"position"];
transl1.removedOnCompletion = FALSE;
transl1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
transl1.fromValue = [NSValue valueWithCGPoint:startPt];
transl1.toValue = [NSValue valueWithCGPoint:endPt];
transl1.duration = 2.0;
transl1.fillMode = kCAFillModeForwards;
transl1.beginTime = 0;
//scale 1
CABasicAnimation *scale1 = [CABasicAnimation
animationWithKeyPath:#"transform.scale"];
scale1.removedOnCompletion = FALSE;
[scale1 setToValue:[NSNumber numberWithInt:3]];
[scale1 setDuration:2.0f];
scale1.fillMode = kCAFillModeForwards;
scale1.beginTime = 0;
//rotation1
CABasicAnimation *rotation1 = [CABasicAnimation
animationWithKeyPath:#"transform.rotation.z"];
rotation1.removedOnCompletion = FALSE;
[rotation1 setFromValue:DegreesToNumber(0)];
[rotation1 setToValue:DegreesToNumber(90)];
//rotation1.repeatCount = HUGE_VALF;
[rotation1 setDuration:2.0f];
rotation1.fillMode = kCAFillModeForwards;
rotation1.beginTime = 0;
//group
CAAnimationGroup* group = [CAAnimationGroup animation];
[group setDuration: 6.0];
group.removedOnCompletion = FALSE;
group.fillMode = kCAFillModeForwards;
[group setAnimations: [NSArray arrayWithObjects:scale1, transl1, rotation1, nil]];
[myLayer addAnimation: group forKey: nil];
}
Your code doesn't repeat the annotation 10 times but starts 10 animations right away. If your goal is to start the animations after the previous one ended you should try using an NSTimer.
You can use an NSTimer to trigger each group of animations at a different time, or you can set the beginTime on each group, with code like this:
group.beginTime = CACurrentMediaTime() + delay;
For animations, using the beginTime gives more accurate timing, since CA is run on a separate thread and doesn't stall like NSTimers if your app gets busy.

CAAnimationGroup reverts to original position on completion

In iOS I'm trying to create the effect of an icon shrinking in size, and flying across the screen in an arc while fading out, and then disappearing. I've achieved these 3 effects with an CAAnimationGroup, and it does what I want. The problem is when the animation ends, the view appears back at the original position, full size and full opacity. Can anyone see what I am doing wrong in the code below?
The animation should not revert to it's original position, but just disappear at the end.
UIBezierPath *movePath = [UIBezierPath bezierPath];
CGPoint libraryIconCenter = CGPointMake(610, 40);
CGPoint ctlPoint = CGPointMake(self.imgViewCropped.center.x, 22.0);
movePath moveToPoint:self.imgViewCropped.center];
[movePath addQuadCurveToPoint:libraryIconCenter
controlPoint:ctlPoint];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = NO;
CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:#"transform"];
scaleAnim.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
scaleAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
scaleAnim.removedOnCompletion = NO;
CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:#"alpha"];
opacityAnim.fromValue = [NSNumber numberWithFloat:1.0];
opacityAnim.toValue = [NSNumber numberWithFloat:0.0];
opacityAnim.removedOnCompletion = NO;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim,scaleAnim,opacityAnim, nil];
animGroup.duration = 0.6;
animGroup.delegate = self;
animGroup.removedOnCompletion = NO;
[self.imgViewCropped.layer addAnimation:animGroup forKey:nil];
I believe you need to set the fillMode property of your animations to kCAFillModeForwards. That should freeze the animations at their end time. Another suggestion (and honestly, this is what I'd usually do) is just se the properties of the layer itself to their final position after you've set up the animation. That way when the animation is removed, the layer will still have the final properties as part of its model.
As an aside, the removedOnCompletion flag of animations contained within a CAAnimationGroup is ignored. You should probably just remove those assignments since they're misleading. Replace them with assignments to fillMode as specified above.

Resources