Chaining keyframe animations - ios

I'm trying to chain two keyframe-based animations, but the second animation won't play for some reason. Any idea what's going on?
// Create an animation group to contain all album art animations
CAAnimationGroup *albumArtAnimationGroup = [CAAnimationGroup animation];
albumArtAnimationGroup.duration = 3.0;
albumArtAnimationGroup.repeatCount = 0;
// First album art translation animation
CGMutablePathRef albumCoverPath = CGPathCreateMutable();
CGPathMoveToPoint(albumCoverPath, NULL,
albumCover.layer.position.x,
albumCover.layer.position.y);
CGPathAddLineToPoint(albumCoverPath, NULL,
albumCover.layer.position.x-[UIScreen mainScreen].bounds.size.height,
albumCover.layer.position.y);
CAKeyframeAnimation *albumCoverTranslationAnimation;
albumCoverTranslationAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
albumCoverTranslationAnimation.calculationMode = kCAAnimationLinear;
albumCoverTranslationAnimation.path = albumCoverPath;
albumCoverTranslationAnimation.duration = 1.0;
// Second album art translation animation
CGMutablePathRef albumCoverPath1 = CGPathCreateMutable();
CGPathMoveToPoint(albumCoverPath1, NULL,
albumCover.layer.position.x+[UIScreen mainScreen].bounds.size.height,
albumCover.layer.position.y);
CGPathAddLineToPoint(albumCoverPath1, NULL,
albumCover.layer.position.x,
albumCover.layer.position.y);
CAKeyframeAnimation *albumCoverTranslationAnimation1;
albumCoverTranslationAnimation1 = [CAKeyframeAnimation animationWithKeyPath:#"position"];
albumCoverTranslationAnimation1.calculationMode = kCAAnimationLinear;
albumCoverTranslationAnimation1.path = albumCoverPath1;
albumCoverTranslationAnimation1.duration = 1.0;
CFTimeInterval localAlbumLayerTime = [albumCover.layer convertTime:CACurrentMediaTime() fromLayer:nil];
albumCoverTranslationAnimation1.beginTime = localAlbumLayerTime + 1.0;
albumArtAnimationGroup.animations = #[albumCoverTranslationAnimation, albumCoverTranslationAnimation1];
[albumCover.layer addAnimation:albumArtAnimationGroup forKey:#"position"];
Edit
Solved. It turns out that either Apple's documentation was misleading, or I was using CACurrentMediaTime incorrectly. The code below did the trick.
albumArtAnimationGroup.duration = 2.0;
albumCoverTranslationAnimation.duration = 1.0;
albumCoverTranslationAnimation1.beginTime = 1;
albumArtAnimationGroup.animations = #[albumCoverTranslationAnimation, albumCoverTranslationAnimation1];
[albumCover.layer addAnimation:albumArtAnimationGroup forKey:#"position"];
However, according to Apple, I may possibly run into issues regarding the timing since I am not using CACurrentMediaTime(), as shown below.
To assist you in making sure time values are appropriate for a given
layer, the CALayer class defines the convertTime:fromLayer: and
convertTime:toLayer: methods. You can use these methods to convert a
fixed time value to the local time of a layer or to convert time
values from one layer to another. The methods take into account the
media timing properties that might affect the local time of the layer
and return a value that you can use with the other layer. Listing 5-3
shows an example that you should use regularly to get the current
local time for a layer. The CACurrentMediaTime function is a
convenience function that returns the computer’s current clock time,
which the method takes and converts to the layer’s local time.
Listing 5-3 Getting a layer’s current local time
CFTimeInterval localLayerTime = [myLayer convertTime:CACurrentMediaTime() fromLayer:nil];

When you add multiple animations to an animation group, the beginTime property starts at 0, and ends at the duration of the animation. So to chain a second animation, set it's beginTime to the duration of the first animation, and make the animation group's duration long enough for the entire animation sequence.
BTW, it might be simpler to create a single CAKeyframeAnimation that has the 2 paths combined together into one. It would be a lot less code.

Related

how to reset/restart an animation and have it appear continuous?

So, I am fairly new to iOS programming, and have inherited a project from a former coworker. We are building an app that contains a gauge UI. When data comes in, we want to smoothly rotate our "layer" (which is a needle image) from the current angle to a new target angle. Here is what we have, which worked well with slow data:
-(void) MoveNeedleToAngle:(float) target
{
static float old_Value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation");
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_Value = target;
}
The problem is we have a new data scheme in which new data can come in (and the above method called) faster, before the animation is complete. What's happening I think is that the animation is restarted from the old target to the new target, which makes it very jumpy.
So I was wondering how to modify the above function to add a continuous/restartable behavior, as follows:
Check if the current animation is in progress and
If so, figure out where the current animation angle is, and then
Cancel the current and start a new animation from the current rotation to the new target rotation
Is it possible to build that behavior into the above function?
Thanks. Sorry if the question seems uninformed, I have studied and understand the above objects/methods, but am not an expert.
Yes you can do this using your existing method, if you add this bit of magic:
- (void)removeAnimationsFromView:(UIView*)view {
CALayer *layer = view.layer.presentationLayer;
CGAffineTransform transform = layer.affineTransform;
[layer removeAllAnimations];
view.transform = transform;
}
The presentation layer encapsulates the actual state of the animation. The view itself doesn't carry the animation state properties, basically when you set an animation end state, the view acquires that state as soon as you trigger the animation. It is the presentation layer that you 'see' during the animation.
This method captures the state of the presentation layer at the exact moment you cancel the animation, and then applies that state to the view.
Now you can use this method in your animation method, which will look something like this:
-(void) MoveNeedleToAngle:(float) target{
[self removeAnimationsFromView:imageView_Needle];
id rotation = [imageView_Needle valueForKeyPath:#"layer.transform.rotation.z"];
CGFloat old_value = [rotation floatValue]*57.2958;
// static float old_value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_value = target;
}
(I have made minimal changes to your method: there are a few coding style changes i would also make, but they are not relevant to your problem)
By the way, I suggest you feed your method in radians, not degrees, that will mean you can remove those 57.2958 constants.
You can get the current rotation from presentation layer and just set the toValue angle. No need to keep old_value
-(void) MoveNeedleToAngle:(float) targetRadians{
CABasicAnimation *animation =[CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.duration=5.0;
animation.fillMode=kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
animation.removedOnCompletion=NO;
animation.fromValue = [NSNumber numberWithFloat: [[layer.presentationLayer valueForKeyPath: #"transform.rotation"] floatValue]];
animation.toValue = [NSNumber numberWithFloat:targetRadians];
// layer.transform= CATransform3DMakeRotation(rads, 0, 0, 1);
[layer addAnimation:animation forKey:#"rotate"];
}
Another way i found (commented line above) is instead of using fromValue and toValue just set the layer transform. This will produce the same animation but the presentationLayer and the model will be in sync.

Sequence animation using CAAnimationGroup

I'm trying to make a sequence of animations, I've found in CAAnimationGroup the right class to achieve that object. In practice I'm adding on a view different subviews and I'd like to animate their entry with a bounce effect, the fact is that I want to see their animations happening right after the previous has finished. I know that I can set the delegate, but I thought that the CAAnimationGroup was the right choice.
Later I discovered that the group animation can belong only to one layer, but I need it on different layers on screen. Of course on the hosting layer doesn't work.
Some suggestions?
- (void) didMoveToSuperview {
[super didMoveToSuperview];
float startTime = 0;
NSMutableArray * animArray = #[].mutableCopy;
for (int i = 1; i<=_score; i++) {
NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject: self.greenLeaf];
UIImageView * greenLeafImageView = [NSKeyedUnarchiver unarchiveObjectWithData: archivedData];
greenLeafImageView.image = [UIImage imageNamed:#"greenLeaf"];
CGPoint leafCenter = calculatePointCoordinateWithRadiusAndRotation(63, -(M_PI/11 * i) - M_PI_2);
greenLeafImageView.center = CGPointApplyAffineTransform(leafCenter, CGAffineTransformMakeTranslation(self.bounds.size.width/2, self.bounds.size.height));
[self addSubview:greenLeafImageView];
//Animation creation
CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform.scale"];
greenLeafImageView.layer.transform = CATransform3DIdentity;
bounceAnimation.values = #[
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:1.1],
[NSNumber numberWithFloat:0.8],
[NSNumber numberWithFloat:1.0]
];
bounceAnimation.duration = 2;
bounceAnimation.beginTime = startTime;
startTime += bounceAnimation.duration;
[animArray addObject:bounceAnimation];
//[greenLeafImageView.layer addAnimation:bounceAnimation forKey:nil];
}
// Rotation animation
[UIView animateWithDuration:1 animations:^{
self.arrow.transform = CGAffineTransformMakeRotation(M_PI/11 * _score);
}];
CAAnimationGroup * group = [CAAnimationGroup animation];
group.animations = animArray;
group.duration = [[ animArray valueForKeyPath:#"#sum.duration"] floatValue];
[self.layer addAnimation:group forKey:nil];
}
CAAnimationGroup is meant for having multiple CAAnimation subclasses being stacked together to form an animation, for instance, one animation can perform an scale, the other moves it around, while a third one can rotate it, it's not meant for managing multiple layers, but for having multiple overlaying animations.
That said, I think the easiest way to solve your issue, is to assign each CAAnimation a beginTime equivalent to the sum of the durations of all the previous ones, to illustrate:
for i in 0 ..< 20
{
let view : UIView = // Obtain/create the view...;
let bounce = CAKeyframeAnimation(keyPath: "transform.scale")
bounce.duration = 0.5;
bounce.beginTime = CACurrentMediaTime() + bounce.duration * CFTimeInterval(i);
// ...
view.layer.addAnimation(bounce, forKey:"anim.bounce")
}
Notice that everyone gets duration * i, and the CACurrentMediaTime() is a necessity when using the beginTime property (it's basically a high-precision timestamp for "now", used in animations). The whole line could be interpreted as now + duration * i.
Must be noted, that if a CAAnimations is added to a CAAnimationGroup, then its beginTime becomes relative to the group's begin time, so a value of 5.0 on an animation, would be 5.0 seconds after the whole group starts. In this case, you don't use the CACurrentMediaTime()
If you review the documentation, you will note that CAAnimationGroup inherits from CAAnimation, and that CAAnimation can only be assigned to one CALayer. It's intent is really to make it easy to create and manage multiple animations you wish to apply to a CALayer at the same time, not to manager animations for multiple CALayer objects.
To handle the sequencing of different animations between different CALayer or UIViewobjects, a technique I use is to create an NSOperation for each object/animation, then throw them into a NSOperationQueue to manage the sequencing. This is a bit complicated as you have to use the animation completion callback to tell the NSOperation it is finished, but if you write a good animation management subclass of NSOperation, it can be rather convenient and allow you to create sophisticated sequencing paths. The low-rent way of accomplishing the sequencing goal is to simply set the beginTime property on your CAAnimation object (which comes from it's adoption of the CAMediaTiming protocol) as appropriate to get the timing you want.
With that said, I am going to point you to some code that I wrote and open-sourced to solve the exact same use case you describe. You may find it on github here (same code included). I will add the following notes:
My animation management code allow your to define your animation in a plist by identifying the sequence and timing of image changes, scale changes, position changes, etc. It's actually pretty convenient and cleaner to adjust your animation in a plist file rather than in code (which is why I wrote this).
If the user is not expected to interact with the subviews you creating, it's actually much better (less overhead) to create layer objects that are added as sub-layers to your hosting view's layer.

Using CABasicAnimation to rotate a UIImageView more than once

I am using a CABasicAnimation to rotate a UIImageView 90 degrees clockwise, but I need to have it rotate a further 90 degrees later on from its position after the initial 90 degree rotation.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = 10;
animation.additive = YES;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.fromValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(0)];
animation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(90)];
[_myview.layer addAnimation:animation forKey:#"90rotation"];
Using the code above works initially, the image stays at a 90 degree angle. If I call this again to make it rotate a further 90 degrees the animation starts by jumping back to 0 and rotating 90 degrees, not 90 to 180 degrees.
I was under the impression that animation.additive = YES; would cause further animations to use the current state as a starting point.
Any ideas?
tl;dr: It is very easy to misuse removeOnCompletion = NO and most people don't realize the consequences of doing so. The proper solution is to change the model value "for real".
First of all: I'm not trying to judge or be mean to you. I see the same misunderstanding over and over and I can see why it happens. By explaining why things happen I hope that everyone who experience the same issues and sees this answer learn more about what their code is doing.
What went wrong
I was under the impression that animation.additive = YES; would cause further animations to use the current state as a starting point.
That is very true and it's exactly what happens. Computers are funny in that sense. They always to exactly what you tell them and not what you want them to do.
removeOnCompletion = NO can be a bitch
In your case the villain is this line of code:
animation.removedOnCompletion = NO;
It is often misused to keep the final value of the animation after the animation completes. The only problem is that it happens by not removing the animation from the view. Animations in Core Animation doesn't alter the underlying property that they are animating, they just animate it on screen. If you look at the actual value during the animation you will never see it change. Instead the animation works on what is called the presentation layer.
Normally when the animation completes it is removed from the layer and the presentation layer goes away and the model layer appears on screen again. However, when you keep the animation attached to the layer everything looks as it should on screen but you have introduced a difference between what the property says is the transform and how the layer appears to be rotated on screen.
When you configure the animation to be additive that means that the from and to values are added to the existing value, just as you said. The problem is that the value of that property is 0. You never change it, you just animate it. The next time you try and add that animation to the same layer the value still won't be changed but the animation is doing exactly what it was configured to do: "animate additively from the current value of the model".
The solution
Skip that line of code. The result is however that the rotation doesn't stick. The better way to make it stick is to change the model. Set the new end value of the rotation before animating the rotation so that the model looks as it should when the animation gets removed.
byValue is like magic
There is a very handy property (that I'm going to use) on CABasicAnimation that is called byValue that can be used to make relative animations. It can be combined with either toValue and fromValue to do many different kinds of animations. The different combinations are all specified in its documentation (under the section). The combination I'm going to use is:
byValue and toValue are non-nil. Interpolates between (toValue - byValue) and toValue.
Some actual code
With an explicit toValue of 0 the animation happens from "currentValue-byValue" to "current value". By changing the model first current value is the end value.
NSString *zRotationKeyPath = #"transform.rotation.z"; // The killer of typos
// Change the model to the new "end value" (key path can work like this but properties don't)
CGFloat currentAngle = [[_myview.layer valueForKeyPath:zRotationKeyPath] floatValue];
CGFloat angleToAdd = M_PI_2; // 90 deg = pi/2
[_myview.layer setValue:#(currentAngle+angleToAdd) forKeyPath:zRotationKeyPath];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:zRotationKeyPath];
animation.duration = 10;
// #( ) is fancy NSNumber literal syntax ...
animation.toValue = #(0.0); // model value was already changed. End at that value
animation.byValue = #(angleToAdd); // start from - this value (it's toValue - byValue (see above))
// Add the animation. Once it completed it will be removed and you will see the value
// of the model layer which happens to be the same value as the animation stopped at.
[_myview.layer addAnimation:animation forKey:#"90rotation"];
Small disclaimer:
I didn't run this code but am fairly certain that it runs as it should and that I didn't do any typos. Correct me if I did. The entire discussion is still valid.
pass incremental value of angle see my code
static int imgAngle=0;
- (void)doAnimation
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = 5;
animation.additive = YES;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.fromValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(imgAngle)];
animation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(imgAngle+90)];
[self.imgView.layer addAnimation:animation forKey:#"90rotation"];
imgAngle+=90;
if (imgAngle>360) {
imgAngle = 0;
}
}
Above code is just for idea. Its not tested

Is there a way to do 3D transforms with keypoints?

I've created a 3D cube view transition that does the following:
Creates a layer container.
Adds the current view in 2D space.
Adds the next view to the right and rotated 90 degrees (like in a cube)
Performs an animation to rotate the cube.
Here's the code for that - its working fine. . . actual problem below:
- (CAAnimation*)makeRotationWithMetrics:(BBRotationMetrics*)metrics
{
[CATransaction flush];
CAAnimationGroup* group = [CAAnimationGroup animation];
group.delegate = self;
group.duration = metrics.duration;
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
group.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
CABasicAnimation* translationX = [CABasicAnimation animationWithKeyPath:#"sublayerTransform.translation.x"];
translationX.toValue = metrics.translationPointsX;
CABasicAnimation* rotation = [CABasicAnimation animationWithKeyPath:#"sublayerTransform.rotation.y"];
rotation.toValue = metrics.radians;
CABasicAnimation* translationZ = [CABasicAnimation animationWithKeyPath:#"sublayerTransform.translation.z"];
translationZ.toValue = metrics.translationPointsZ;
group.animations = [NSArray arrayWithObjects:rotation, translationX, translationZ, nil];
return group;
}
At the end of the animation, I would like to perform a jiggle, as though the cube was mounted on a spring. I've tried playing a series of animations to rotate around the Y axis, gradually decaying.
I've set up a series of animations, but each ones starts from the origin, instead of the last point the layer got to. . . how can I fix that? One way would be to set the tranform on the layer first,
But is there a way to do 3D transforms with key-points ?
It sounds to me like you are looking for CAKeyframeAnimation. Instead of specifying a toValue you pass an array of values and optionally keyTimes including start and end values.
The starts from the origin problem comes from using removedOnCompletion. By doing so you are introducing a difference between the presentation (what is shown on screen) and the model (the property value of your layer). A cleaner approach is to explicitly set the end value and animate from the previous value to the end value. That will leave you in a clean state when the animation finishes.

how to speed up CABasicAnimation?

I am using the following CABasicAnimation. But, its very slow..is there a way to speed it up ? Thanks.
- (void)spinLayer:(CALayer *)inLayer duration:(CFTimeInterval)inDuration
direction:(int)direction
{
CABasicAnimation* rotationAnimation;
// Rotate about the z axis
rotationAnimation =
[CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
// Rotate 360 degress, in direction specified
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0 * direction];
// Perform the rotation over this many seconds
rotationAnimation.duration = inDuration;
// Set the pacing of the animation
rotationAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Add animation to the layer and make it so
[inLayer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
Core Animation animations can repeat a number of times, by setting the repeatCount property on the animation.
So if you'd like to have an animation run for a total 80 seconds, you need to figure out a duration for one pass of the animation – maybe one full spin of this layer – and then set the duration to be that value. Then let the animation repeat that full spin several times to fill out your duration.
So something like this:
rotationAnimation.repeatCount = 8.0;
Alternatively, you can use repeatDuration to achieve a similar affect:
rotationAnimation.repeatDuration = 80.0;
In either case, you need to set the duration to the time of a single spin, and then repeat it using ONE of these methods. If you set both properties, the behavior is undefined. You can check out the documentation on CAMediaTiming here.

Resources