I have a CAEmitterLayer and I'd like to have a simple animation run over the course of each particle's life.
As soon as particle pops in, I'd like it to scale up to about 1.2, then after a short time have it scale back to 1.0 and stay that way until it's lifetime expires.
I know about the scale, scaleRange and scaleSpeed properties of the CAEmitterCell but they're way too random for what I need.
Is this possible to do? I've tried adding a CABasicAnimation like this (my CAEmitterCell's name is "heart"):
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"emitterCells.heart.scale"];
anim.fromValue = #(1.0);
anim.toValue = #(2.0);
anim.duration = 3.0;
anim.fillMode = kCAFillModeForwards;
anim.repeatCount = CGFLOAT_MAX;
[self.heartsEmitter addAnimation:anim forKey:#"scaleAnimation"];
but it doesn't work, the particles just appear at a random scale, they don't animate at all.
I'm not completely sure, but it seems to me that you are applying the animation to the emitter instead of the cells.
If the CAEmitterCell's name is heart try this: [self.heart addAnimation:anim forKey:#"scaleAnimation"];. Does this help?
I have one animation, and I want it to be added to two layers:
[view1.layer addAnimation:theAnimation
forKey:#"layer1_animation"];
[view2.layer addAnimation:theAnimation
forKey:#"layer2_animation"];
But when I run my application, only the 1'st view is animating. As documentation tells, layer copies its animations:
This object is copied by the render tree, not referenced. Therefore, subsequent modifications to the object are not propagated into the render tree.
So my code supposed to work correctly. Is it a bug or I'm doing it wrong?
More code:
CGFloat currentY = self.view1.layer.position.y;
CABasicAnimation *yAnimation = [CABasicAnimation animationWithKeyPath:#"position.y"];
yAnimation.fromValue = #(currentY - 2.f);
yAnimation.toValue = #(currentY + 2.f);
yAnimation.duration = 5.0;
yAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
yAnimation.repeatCount = HUGE_VALF;
yAnimation.autoreverses = YES;
yAnimation.fillMode = kCAFillModeBackwards;
[self.view1.layer addAnimation:yAnimation
forKey:#"view1_y_animation"];
[self.view2.layer addAnimation:yAnimation
forKey:#"view2_y_animation"];
The problem was in my code. When initialised fromValue and toValue of the animation, I used y-position of view1. So view1 covered view2 and it was invisible.
I'm trying to chain the animation of a layer and its sublayer. However, the problem I'm having is that the model update of the sublayer's position for its animation is visually apparent during its superlayer's animation. Here's the code I'm using to chain animations:
// Superlayer.
CFTimeInterval now = [self.artworkContainer.layer convertTime:CACurrentMediaTime() fromLayer:nil];
CABasicAnimation *slideDown = [CABasicAnimation animationWithKeyPath:#"position"];
slideDown.duration = SLIDE_DOWN_DURATION; //SLIDE_DOWN_DURATION;
slideDown.beginTime = now;
slideDown.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
slideDown.fromValue = [self.artworkContainer.layer valueForKey:#"position"];
CGPoint finalPointOfContainer = CGPointMake(self.artworkContainer.layer.position.x, self.artworkContainer.layer.position.y+verticalDistanceOfFirstFall);
slideDown.toValue = [NSValue valueWithCGPoint:finalPointOfContainer];
self.artworkContainer.layer.position = finalPointOfContainer;
[self.artworkContainer.layer addAnimation:slideDown forKey:#"position"];
// Sublayer
CABasicAnimation *moveActualArtwork = [CABasicAnimation animationWithKeyPath:#"position"];
moveActualArtwork.duration = timeOfSecondFall;
moveActualArtwork.beginTime = now + SLIDE_DOWN_DURATION;
moveActualArtwork.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
moveActualArtwork.fromValue = [self.artWork.layer valueForKey:#"position"];
CGPoint finalPointOfArtwork = CGPointMake(self.artWork.layer.position.x, self.artWork.layer.position.y+verticalDistanceOfSecondFall);
moveActualArtwork.toValue = [NSValue valueWithCGPoint:finalPointOfArtwork];
self.artWork.layer.position = finalPointOfArtwork; // If this is commented out, the superlayer animation looks correct (of course this isn't a true fix because the sublayer snaps back to its original position without a model update).
moveActualArtwork.delegate = self;
[self.artWork.layer addAnimation:moveActualArtwork forKey:#"position"];
I found the answer as I was writing the question. I needed to set the fillMode property of the second animation: moveActualArtwork.fillMode = kCAFillModeBackwards;
If one were to think of the fromValue and toValue as positions on a timeline, the problem was that before the sublayer hit the fromValue spot on the timeline, it was working off of its model layer to determine its position. By using kCAFillModeBackwards, the fromValue is effectively extended to cover the timeline previous to it so iOS knows what to render before the animation actually begins.
I found the answer reviewing the WWDC 2011 video Session 421 - Core Animation Essentials, at 45:31.
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.
I am rotating a CALayer and trying to stop it at its final position after animation is completed.
But after animation completes it resets to its initial position.
(xcode docs explicitly say that the animation will not update the value of the property.)
any suggestions how to achieve this.
Here's the answer, it's a combination of my answer and Krishnan's.
cabasicanimation.fillMode = kCAFillModeForwards;
cabasicanimation.removedOnCompletion = NO;
The default value is kCAFillModeRemoved. (Which is the reset behavior you're seeing.)
The problem with removedOnCompletion is the UI element does not allow user interaction.
I technique is to set the FROM value in the animation and the TO value on the object.
The animation will auto fill the TO value before it starts, and when it's removed will leave the object at it's correct state.
// fade in
CABasicAnimation *alphaAnimation = [CABasicAnimation animationWithKeyPath: #"opacity"];
alphaAnimation.fillMode = kCAFillModeForwards;
alphaAnimation.fromValue = NUM_FLOAT(0);
self.view.layer.opacity = 1;
[self.view.layer addAnimation: alphaAnimation forKey: #"fade"];
Core animation maintains two layer hierarchies: the model layer and the presentation layer. When the animation is in progress, the model layer is actually intact and keeps it initial value. By default, the animation is removed once the it's completed. Then the presentation layer falls back to the value of the model layer.
Simply setting removedOnCompletion to NO means the animation won't be removed and wastes memory. In addition, the model layer and the presentation layer won't be synchronous any more, which may lead to potential bugs.
So it would be a better solution to update the property directly on the model layer to the final value.
self.view.layer.opacity = 1;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.fromValue = 0;
animation.toValue = 1;
[self.view.layer addAnimation:animation forKey:nil];
If there's any implicit animation caused by the first line of above code, try to turn if off:
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.view.layer.opacity = 1;
[CATransaction commit];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.fromValue = 0;
animation.toValue = 1;
[self.view.layer addAnimation:animation forKey:nil];
Reference:
Animations Explained by objc.io.
"iOS 7 Programming Pushing the Limits" by Rob Napier and Mugunth Kumar.
Set the following property:
animationObject.removedOnCompletion = NO;
You can simply set the key of CABasicAnimation to position when you add it to the layer. By doing this, it will override implicit animation done on the position for the current pass in the run loop.
CGFloat yOffset = 30;
CGPoint endPosition = CGPointMake(someLayer.position.x,someLayer.position.y + yOffset);
someLayer.position = endPosition; // Implicit animation for position
CABasicAnimation * animation =[CABasicAnimation animationWithKeyPath:#"position.y"];
animation.fromValue = #(someLayer.position.y);
animation.toValue = #(someLayer.position.y + yOffset);
[someLayer addAnimation:animation forKey:#"position"]; // The explicit animation 'animation' override implicit animation
You can have more information on 2011 Apple WWDC Video Session 421 - Core Animation Essentials (middle of the video)
just put it inside your code
CAAnimationGroup *theGroup = [CAAnimationGroup animation];
theGroup.fillMode = kCAFillModeForwards;
theGroup.removedOnCompletion = NO;
A CALayer has a model layer and a presentation layer. During an animation, the presentation layer updates independently of the model. When the animation is complete, the presentation layer is updated with the value from the model. If you want to avoid a jarring jump after the animation ends, the key is to keep the two layers in sync.
If you know the end value, you can just set the model directly.
self.view.layer.opacity = 1;
But if you have an animation where you don't know the end position (e.g. a slow fade that the user can pause and then reverse), then you can query the presentation layer directly to find the current value, and then update the model.
NSNumber *opacity = [self.layer.presentationLayer valueForKeyPath:#"opacity"];
[self.layer setValue:opacity forKeyPath:#"opacity"];
Pulling the value from the presentation layer is also particularly useful for scaling or rotation keypaths. (e.g. transform.scale, transform.rotation)
So my problem was that I was trying to rotate an object on pan gesture and so I had multiple identical animations on each move. I had both fillMode = kCAFillModeForwards and isRemovedOnCompletion = false but it didn't help. In my case, I had to make sure that the animation key is different each time I add a new animation:
let angle = // here is my computed angle
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.toValue = angle
rotate.duration = 0.1
rotate.isRemovedOnCompletion = false
rotate.fillMode = CAMediaTimingFillMode.forwards
head.layer.add(rotate, forKey: "rotate\(angle)")
This works:
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 0.3
someLayer.opacity = 1 // important, this is the state you want visible after the animation finishes.
someLayer.addAnimation(animation, forKey: "myAnimation")
Core animation shows a 'presentation layer' atop your normal layer during the animation. So set the opacity (or whatever) to what you want to be seen when the animation finishes and the presentation layer goes away. Do this on the line before you add the animation to avoid a flicker when it completes.
If you want to have a delay, do the following:
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 0.3
animation.beginTime = someLayer.convertTime(CACurrentMediaTime(), fromLayer: nil) + 1
animation.fillMode = kCAFillModeBackwards // So the opacity is 0 while the animation waits to start.
someLayer.opacity = 1 // <- important, this is the state you want visible after the animation finishes.
someLayer.addAnimation(animation, forKey: "myAnimation")
Finally, if you use 'removedOnCompletion = false' it'll leak CAAnimations until the layer is eventually disposed - avoid.
Without using the removedOnCompletion
You can try this technique:
self.animateOnX(item: shapeLayer)
func animateOnX(item:CAShapeLayer)
{
let endPostion = CGPoint(x: 200, y: 0)
let pathAnimation = CABasicAnimation(keyPath: "position")
//
pathAnimation.duration = 20
pathAnimation.fromValue = CGPoint(x: 0, y: 0)//comment this line and notice the difference
pathAnimation.toValue = endPostion
pathAnimation.fillMode = kCAFillModeBoth
item.position = endPostion//prevent the CABasicAnimation from resetting item's position when the animation finishes
item.add(pathAnimation, forKey: nil)
}
Simply setting fillMode and removedOnCompletion didn't work for me. I solved the problem by setting all of the properties below to the CABasicAnimation object:
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"transform"];
ba.duration = 0.38f;
ba.fillMode = kCAFillModeForwards;
ba.removedOnCompletion = NO;
ba.autoreverses = NO;
ba.repeatCount = 0;
ba.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.85f, 0.85f, 1.0f)];
[myView.layer addAnimation:ba forKey:nil];
This code transforms myView to 85% of its size (3rd dimension unaltered).
#Leslie Godwin's answer is not really good, "self.view.layer.opacity = 1;" is done immediately (it takes about one second), please fix alphaAnimation.duration to 10.0, if you have doubts.
You have to remove this line.
So, when you fix fillMode to kCAFillModeForwards and removedOnCompletion to NO, you let the animation remains in the layer. If you fix the animation delegate and try something like:
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
[theLayer removeAllAnimations];
}
...the layer restores immediately at the moment you execute this line. It's what we wanted to avoid.
You must fix the layer property before remove the animation from it. Try this:
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
if([anim isKindOfClass:[CABasicAnimation class] ]) // check, because of the cast
{
CALayer *theLayer = 0;
if(anim==[_b1 animationForKey:#"opacity"])
theLayer = _b1; // I have two layers
else
if(anim==[_b2 animationForKey:#"opacity"])
theLayer = _b2;
if(theLayer)
{
CGFloat toValue = [((CABasicAnimation*)anim).toValue floatValue];
[theLayer setOpacity:toValue];
[theLayer removeAllAnimations];
}
}
}
The easiest solution is to use implicit animations. This will handle all of that trouble for you:
self.layer?.backgroundColor = NSColor.red.cgColor;
If you want to customize e.g. the duration, you can use NSAnimationContext:
NSAnimationContext.beginGrouping();
NSAnimationContext.current.duration = 0.5;
self.layer?.backgroundColor = NSColor.red.cgColor;
NSAnimationContext.endGrouping();
Note: This is only tested on macOS.
I initially did not see any animation when doing this. The problem is that the layer of a view-backed layer does not implicit animate. To solve this, make sure you add a layer yourself (before setting the view to layer-backed).
An example how to do this would be:
override func awakeFromNib() {
self.layer = CALayer();
//self.wantsLayer = true;
}
Using self.wantsLayer did not make any difference in my testing, but it could have some side effects that I do not know of.
It seems that removedOnCompletion flag set to false and fillMode set to kCAFillModeForwards doesn't work for me either.
After I apply new animation on a layer, an animating object resets to its initial state and then animates from that state.
What has to be done additionally is to set the model layer's desired property according to its presentation layer's property before setting new animation like so:
someLayer.path = ((CAShapeLayer *)[someLayer presentationLayer]).path;
[someLayer addAnimation:someAnimation forKey:#"someAnimation"];
Here is a sample from playground:
import PlaygroundSupport
import UIKit
let resultRotation = CGFloat.pi / 2
let view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 300.0))
view.backgroundColor = .red
//--------------------------------------------------------------------------------
let rotate = CABasicAnimation(keyPath: "transform.rotation.z") // 1
rotate.fromValue = CGFloat.pi / 3 // 2
rotate.toValue = resultRotation // 3
rotate.duration = 5.0 // 4
rotate.beginTime = CACurrentMediaTime() + 1.0 // 5
// rotate.isRemovedOnCompletion = false // 6
rotate.fillMode = .backwards // 7
view.layer.add(rotate, forKey: nil) // 8
view.layer.setAffineTransform(CGAffineTransform(rotationAngle: resultRotation)) // 9
//--------------------------------------------------------------------------------
PlaygroundPage.current.liveView = view
Create an animation model
Set start position of the animation (could be skipped, it depends on your current layer layout)
Set end position of the animation
Set animation duration
Delay animation for a second
Do not set false to isRemovedOnCompletion - let Core Animation clean after the animation is finished
Here is the first part of the trick - you say to Core Animation to place your animation to the start position (you set in step #2) before the animation has even been started - extend it backwards in time
Copy prepared animation object, add it to the layer and start the animation after the delay (you set in step #5)
The second part is to set correct end position of the layer - after the animation is deleted your layer will be shown at the correct place