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

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];
}

Related

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.

Glitches when queuing CAAnimations

I have a CAShapeLayer where I animate a circle. The animation is to first "undraw" the circle clockwise and then redraw the circle clockwise. Sort of a "rotating circle". Another way to put it: Move path stroke end point to start, then move the start point to the end.
The animation itself works, but it produces glitches now and then. It manifests in a short glimpse of the entire circle when it is supposed to be "undrawn".
Why does this occur and how can you fix it?
Thanks,
// Shape creation
layer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.width - 2 * OUTER_BORDER_WIDTH, self.width - 2* OUTER_BORDER_WIDTH)].CGPath;
// Animation queuing
-(void) applyNextAnimation
{
CABasicAnimation* animation;
if (self.animatingOpening)
{
animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = NO;
}
else
{
animation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = YES;
}
animation.duration = 1.0f;
animation.autoreverses = NO;
animation.delegate = self;
animation.removedOnCompletion = YES;
[self.outerCircleLayer addAnimation:animation forKey:#"stroke"];
}
// Animation stop callback
-(void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
if (self.isAnimating)
{
[self applyNextAnimation];
}
}
It blinks becuase you are not setting the corresponding property on the layer. So when the animation completes, the layer's model is still in the pre-animated state and that is what you see momentarily between the two animations.
This will get you towards what you want...
if (self.animatingOpening)
{
self.outerCircleLayer.strokeStart = 0.0;
animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = NO;
}
else
{
self.outerCircleLayer.strokeStart = 1.0;
animation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = YES;
}
animation.duration = 1.0f;
animation.autoreverses = NO;
that almost works, but you will notice a more subtle glitch as you transition from the undrawn state to start animating the drawing state. The beginning of the circle has a small reverse animation as it starts. This is an implicit animation triggered by setting strokeStart from 1.0 to 0.0: which you need to get rid of so that all of the animations effects are under your control. You can achieve that most simply by setting disableActions to YES on CATransaction:
[CATransaction setDisableActions:YES];
( add it just above if (self.animatingOpening))

make animation continues on multi layers by using CAAnimation

I'm creating a tutorial screen to let user understand what to do in the screen.
For example, I have viewPlug and viewBulb. First, viewPlug moves up until it collide viewBulb. After that viewBulb changes color to green to notify. After some seconds, viewPlug moves down and viewBulb changes to red immediately.
So far, I'm digging in UIViewAnimation and UIViewKeyframeAnimation but none of them give me full right to handle complex animation, duration, beginTime... Only CAAnimation can do exactly what I want in scenarios.
But I'm not sure if this is a best way to do in my case, or iOS supports other methods that easier/simpler to handle?
Here is my sample code.
- (void)makeAnimations {
// Group all animation into 1 transaction
[CATransaction begin];
// Create plug animation
CABasicAnimation *animatePlug = [CABasicAnimation animationWithKeyPath:#"position.y"];
animatePlug.beginTime = 0;
animatePlug.byValue = #(-180);
animatePlug.duration = 1.5;
animatePlug.fillMode =kCAFillModeForwards;
animatePlug.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
// Create unplug animation
CABasicAnimation *animateUnplug = [CABasicAnimation animationWithKeyPath:#"position.y"];
animateUnplug.beginTime = 4;
animateUnplug.byValue = #100;
animateUnplug.duration = 0.25;
animateUnplug.fillMode =kCAFillModeForwards;
animateUnplug.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
// Group all animation of view A
CAAnimationGroup *animateGroupPosition = [CAAnimationGroup animation];
animateGroupPosition.duration = 6;
animateGroupPosition.animations = #[animatePlug, animateUnplug];
animateGroupPosition.repeatCount = HUGE_VALF;
[self.viewPlug.layer addAnimation:animateGroupPosition forKey:#"position"];
// Create plug color animation
CABasicAnimation *animateGreen = [CABasicAnimation animationWithKeyPath:#"backgroundColor"];
animateGreen.toValue = (__bridge id)([UIColor greenColor].CGColor);
animateGreen.beginTime = 1.5;
animateGreen.duration = 0.5;
animateGreen.fillMode =kCAFillModeForwards;
// Create unplug color animation
CABasicAnimation *animateRed = [CABasicAnimation animationWithKeyPath:#"backgroundColor"];
animateRed.toValue = (__bridge id)([UIColor redColor].CGColor);
animateRed.beginTime = 4;
animateRed.duration = 0.1;
animateRed.fillMode =kCAFillModeForwards;
// Group all animation of view B
CAAnimationGroup *animateGroupColor = [CAAnimationGroup animation];
animateGroupColor.duration = 6;
animateGroupColor.animations = #[animateGreen, animateRed];
animateGroupColor.repeatCount = HUGE_VALF;
[self.viewBulb.layer addAnimation:animateGroupColor forKey:#"color"];
[CATransaction commit];
}

Synchronising image, text, and positioning with CoreAnimation

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

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