CABasicAnimation duration & keeping layer position - ios

I'm trying to make very basic animation for UIButton. The goal is rotate layer 180 degrees. Here is my animation code which is called from beginTrackingWithTouch:
private func rotateButton() {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.timingFunction = CAMediaTimingFunction(name: "easeIn")
rotationAnimation.toValue = M_PI
rotationAnimation.duration = CollapseButton.kCollapseButtonAnimationDuration
rotationAnimation.repeatCount = 1
rotationAnimation.cumulative = true
layer.addAnimation(rotationAnimation, forKey: "rotationAnimation")
}
Now I'd like to add collapsing view animation when tapping this button. In my VC:
__weak __typeof(self) weakSelf = self;
[UIView animateWithDuration:CollapseButton.kCollapseButtonAnimationDuration animations:^{
CGRect currentFrame = weakSelf.frame;
currentFrame.size.height = 20;
weakSelf.frame = currentFrame;
}];
I have 2 questions:
After button finishes its animation it resets layer position. So, if arrow were showing top, it animated to showing down and finally resets to top. How can I preserve layer orientation?
As you can see animation duration and timing functions are the same. For the reason I cannot understand UIView animates much slower. Any ideas?

Core animation is strange. The animation creates a "presentation layer" that generates the appearance of the change you are animating, but does not actually change the property in question.
In order to get your animation to finish with the object at the end state, you should set both a fromValue (at the starting setting) and a toValue, and then set the property to it's ending value after submitting the animation:
private func rotateButton() {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.timingFunction = CAMediaTimingFunction(name: "easeIn")
rotationAnimation.fromValue = 0.0 //<<--- NEW CODE
rotationAnimation.toValue = M_PI
rotationAnimation.duration = CollapseButton.kCollapseButtonAnimationDuration
rotationAnimation.repeatCount = 1
rotationAnimation.cumulative = true
layer.addAnimation(rotationAnimation, forKey: "rotationAnimation")
layer.transform = CATransformMakeRotation(M_PI) //<<--- NEW CODE
}
You can also set the animation's removeWhenFinished property to true, but that has other complications.
BTW, you should not try to manipulate the frame of a view that has a non-identity transform. Instead, set the scale on the view's transform.
I'm not sure why the 2 animations are taking different amounts of time. I do notice that you are setting the CAAnimation's timing function to easeIn, but leaving the UIView's timing function as the default (ease-in, ease-out.) That will create animations that don't look the same. You should probably set your view animation to use easeIn timing as well. (To do that you'll need to use the longer form animateWithDuration:delay:options:animations:completion:)

Related

CAAnimation is overriding previous CAAnimation animation

I have one this scenario:
I add to a layer CAAnimation that transforms it to a specific frame. with a starting time of 0.
Then I add another CAAnimation that transforms it to a different frame. with a starting time of 0.5.
what happens is that the layer immediately gets the second frame (with no animation) and after the first animation time passes the second animation is completed correctly.
This is the animation creation code:
+ (CAAnimation *)transformAnimation:(CALayer *)layer
fromFrame:(CGRect)fromFrame
toFrame:(CGRect)toFrame
fromAngle:(CGFloat)fromAngle
toAngle:(CGFloat)toAngle
anchor:(CGPoint)anchor
vertical:(BOOL)vertical
begin:(CFTimeInterval)begin
duration:(CFTimeInterval)duration {
CATransform3D fromTransform = makeTransform(layer, fromFrame, fromAngle, anchor, vertical);
CATransform3D midTransform1 = makeTransformLerp(layer, fromFrame, toFrame, fromAngle, toAngle, anchor, 0.33, vertical);
CATransform3D midTransform2 = makeTransformLerp(layer, fromFrame, toFrame, fromAngle, toAngle, anchor, 0.66, vertical);
CATransform3D toTransform = makeTransform(layer, toFrame, toAngle, anchor, vertical);
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
animation.values = #[[NSValue valueWithCATransform3D:fromTransform],
[NSValue valueWithCATransform3D:midTransform1],
[NSValue valueWithCATransform3D:midTransform2],
[NSValue valueWithCATransform3D:toTransform]
];
animation.beginTime = begin;
animation.duration = duration;
animation.fillMode = kCAFillModeBoth;
animation.calculationMode = kCAAnimationPaced;
animation.removedOnCompletion = NO;
return animation;
}
EDIT
in most scenarios, this code works well and the animations are sequenced correctly. But if I set 1 transform animation to start after 2 seconds and then set another transform to start after 4 seconds. the first transform is applied immediately to the layer and the second animation starts from there.
Any Idea how can I separate the animation to run one after the other?
(I prefer not using a completion block)
Thanks
The easiest and most glaring early fix would be to change the fill mode so that the second animation is not clamped on both ends overriding the previous animation.
animation.fillMode = kCAFillModeForwards;
Also I would adjust the begin time to be
animation.beginTime = CACurrentMediaTime() + begin;
If it is a matter of overlapping begin times and durations and not this let me know and I can provide that as well.

CABasicAnimation change startingpoint to go forward and backwards

I'm trying to make a small ui controlled animation (as described in this answer) and continue this animation after rotation. Due to autolayout issues I have to remove and add the whole layer on autorotate.
This works so far. My problem is, as I want continue the animation from the same position it was left off, it won't go anywhere prior to the state it was the moment it rotated.
e.g. the slider is at 0.5. The animation was added again (due to removal) and I've set the timeOffset corresponding to 0.5. The animation will go forward, but not backward.
I create my animation via:
let animatePhase = CABasicAnimation(keyPath: "lineDashPhase")
animatePhase.byValue = phaseLength
animatePhase.duration = 1.0
animatePhase.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animatePhase.repeatCount = Float.infinity
lineLayer.addAnimation(animatePhase, forKey: "marching ants")
lineLayer.speed = 0.0
lineLayer.timeOffset = 0.0;
lineLayer.beginTime = 0.0;
lineLayer is a CAShapeLayer.
On `layoutSubviews' I remove and recreate the layer.
What am I doing wrong?
Thanks

CALayer - CABasicAnimation not scaling around center/anchorPoint

Using the code below I've been trying to get my CALayer to scale around its center - but it scales by left/top (0,0). I saw this question: Anchor Point in CALayer and have tried setting the anchorPoint - but it was [.5, .5] before I set it and setting makes no difference. Also tried setting contentsGravity.
The set up is I have a CAShapeLayer added to self.view.layer I do some drawing in it and then add the CABasicAnimation. The animation runs fine - but it scales toward the top/left - not the center of the view.
Have been tinkering with this for a few hours - it must be something simple but I'm not getting it.
shapeLayer.anchorPoint = CGPointMake(.5,.5);
shapeLayer.contentsGravity = #"center";
printf("anchorPoint %f %f\n", shapeLayer.anchorPoint.x, shapeLayer.anchorPoint.y);
CABasicAnimation *animation =
[CABasicAnimation animationWithKeyPath:#"transform.scale"];
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
[animation setDuration:1.0];
animation.fillMode=kCAFillModeForwards;
animation.removedOnCompletion=NO;
[shapeLayer addAnimation:animation forKey:#"zoom"];
What is the bounds of your layer?
I bet it has zero size; try setting its size to the same size as your shape.
CAShapeLayer draws the shape as kind of an overlay, independent of the contents (and bounds and contentsGravity and etc.) that you'd use in an ordinary CALayer. It can be a bit confusing.
Also make sure that you're adding the animation after the view is created. If you're trying to add animation in the viewDidLoad, it probably won't work. Try adding it to viewDidAppear instead.
I only know Swift, but here's an example:
override func viewDidAppear (animated: Bool) {
addPulsation(yourButton)
}
func addPulsation (button: UIButton) {
let scaleAnimation:CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 1.0
scaleAnimation.repeatCount = 100
scaleAnimation.autoreverses = true
scaleAnimation.fromValue = 1.05;
scaleAnimation.toValue = 0.95;
button.layer.addAnimation(scaleAnimation, forKey: "scale")
}
It's an old thread but in case this helps anyone.
This ia a coordinate space problem. You just need to set the drawing space and the space of the the path. This code works for me...
...just put this at the start of the animation creation method (inside a method on the parent view)...
// add animation to remap (without affecting other elements)
let animationLayer = CALayer()
layer?.addSublayer(animationLayer). // (don't use optional for iOS)
// set the layer origin to the center of the view (or any center point for the animation)
animationLayer.bounds.origin .x = -self.bounds.size.width / 2.0
animationLayer.bounds.origin .y = -self.bounds.height / 2.0
// make image layer with circle around the zero point
let imageLayer = CAShapeLayer()
animationLayer.addSublayer(imageLayer)
let shapeBounds = CGRect(x: -bounds.size.width/2.0,
y:-bounds.size.height/2.0,
width: bounds.size.width,
height: bounds.size.height)
imageLayer.path = CGPath(ellipseIn: shapeBounds, transform: nil)
imageLayer.fillColor = fillColour
// **** apply your animations to imageLayer here ****
this code works unchanged on iOS and MacOS - apart from the optional needed on layer for MacOS.
(BTW you need to remove the animation layer after the animation finishes!)
I spent hours to find out how to do it according the center but couldn't find any solutions for OSX. I decided to to move layer using CATransform3DMakeTranslation and combine both transforms.
My code:
NSView *viewToTransform = self.view;
[viewToTransform setWantsLayer:YES];
viewToTransform.layer.sublayerTransform = CATransform3DConcat(CATransform3DMakeScale(-1.0f, -1.0f, 1.0f), CATransform3DMakeTranslation(viewToTransform.bounds.size.width, viewToTransform.bounds.size.height, 0));

Core Animation is not working with "alpha" value

Before this code, my movie pic alpha is set to 0,
CABasicAnimation* fadein= [CABasicAnimation animationWithKeyPath:#"alpha"];
[fadein setToValue:[NSNumber numberWithFloat:1.0]];
[fadein setDuration:0.5];
[[moviepic layer]addAnimation:fadein forKey:#"alpha"];
Nothing happened, if I set alpha to 0.5 beforehand instead, the alpha remains at 0.5 and not animating to 1.
I've seen a code using UIView beginAnimations: around, but I'm teaching core animation so I wondered why CABasicAnimation can't do simple task like this?
[CABasicAnimation animationWithKeyPath:#"opacity"];
UIView exposes this as alpha where as CALayer exposes this as opacity.
For Swift:
let opacity = CABasicAnimation(keyPath: "opacity")
opacity.fromValue = fromValue
opacity.toValue = toValue
opacity.duration = duration
opacity.beginTime = CACurrentMediaTime() + beginTime //If a delay is needed
view.layer.add(opacity, forKey: nil)
If you want to keep the final alpha value, you have to set the current view controller as the delegate of the opacity animation:
opacity.delegate = self
And, in the delegate function animationDidStop, you should do:
extension ViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
view.alpha = toValue
}
}
#ohho answers the posted question. Mine will be a bit more generic. For a list what can and how be animated with CABasicAnimation please refer to Apple's documentation

CABasicAnimation resets to initial value after animation completes

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

Resources