Core Animation: Pause before autoreversing? - ios

I have an animation that animates a layer along a curved path and then back. I would like the layer to pause before the auto-reversing kicks in. Is there an easy way to do this with Core Animation?
Here is the code I'm using to start the animation.
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animation.duration = animationDuration;
animation.path = curvedPath;
animation.rotationMode = kCAAnimationRotateAuto;
animation.delegate = nil;
animation.autoreverses = YES;
[self.myView.layer addAnimation:animation forKey:#"cardAnimation"];

The only way I can think of to pause in an auto-reversing animation is to start a timer for 1/2 the animation time.
When the timer fires, set the speed of your animation to 0, and launch another timer. When the second timer fires, set the speed of your animation back to 1 (or whatever it was before) again. That should look good if you're using ease-in, ease-out animation timing, since the animation slows to a stop before reversing.
Failing that, you'd probably have to create separate forward and reverse animations, set a delegate for the animation, and in the completion method, pause and then start the reverse animation.

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.

How to properly set the model when doing CABeginAnimation

I have a UIImageView that when the user taps it, a border of 4 points toggles on and off. I'm trying to animate the border in and out as follows:
CABasicAnimation *widthAnimation = [CABasicAnimation animationWithKeyPath:#"borderWidth"];
widthAnimation.toValue = self.isSelected ? #4.0 : #0.0;
widthAnimation.duration = 0.1;
[self.imageView.layer addAnimation:widthAnimation forKey:#"borderWidth"];
Now, as I've learned from research and scouring SO, CABasicAnimation just changes the presentation layer, but not the actual model. I've also read that using fillMode and removedOnCompletion is bad practice, since it leads to inconsistencies between the model and what the user sees. So, I tried to change the model with the following line:
self.imageView.layer.borderWidth = self.isSelected ? 4.0 : 0.0;
The problem is, this line seems to set the property straight away, so by the time the animation kicks in, the border width is already at it's desired value. I've tried sticking this line at the beginning of the code, end, and everywhere in between, but to no success. I did manage to find a hacky solution: instead of setting the property, I passed the property setter to performSelector: withObject: afterDelay:, with the delay being the duration of the animation. This works most of the time, but sometimes the cycles don't quite match up, and the animation will run first, then it jumps back to the original state, then it snaps to the new state, presumably as a result of performSelector
So is there any way to smoothly animate a border without performSelector?
Any help is greatly appreciated.
Here is an example of CABasicAnimation I made a while ago :
-(void) animateProgressFrom:(CGFloat)fromValue to:(CGFloat)toValue
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.fromValue = #(fromValue);
animation.toValue = #(toValue);
animation.duration = ABS(toValue - fromValue)*3.0;
[self.layer addAnimation:animation forKey:#"opacity"];
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.layer.opacity = toValue;
[CATransaction commit];
}
I think what you needed is the CATransaction at the end of the layer animation.

CABasicAnimation stop animation on completion - iOS

I have an iOS app which is using a CABasicAnimation on repeat:
CABasicAnimation *fadeAnim = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeAnim.fromValue = [NSNumber numberWithFloat:1.0];
fadeAnim.toValue = [NSNumber numberWithFloat:0.2];
fadeAnim.duration = 1.0;
fadeAnim.autoreverses = YES;
fadeAnim.repeatCount = INFINITY;
[colourbutton.titleLabel.layer addAnimation:fadeAnim forKey:#"opacity"];
I have a button which when pressed is meant to stop the animation.
-(IBAction)stopAnim {
[colourbutton.titleLabel.layer removeAllAnimations];
}
It works fine but one thing I am noticing is that is stops the animation suddenly, it doesn't let the animation finish. So how can I get it to finish the current animation and then stop. (Or in other words how can I get it to removeAllAnimations....withAnimation?).
On a side note, do I need to include CoreAnimation framework for this to work. So far the animation is running and I havn't imported the CoreAnimation framework.
Thanks, Dan.
Just add another animation and after that remove the first one like this:
CABasicAnimation *endAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
endAnimation.fromValue = #(((CALayer *)colourbutton.titleLabel.layer.presentationLayer).opacity);
endAnimation.toValue = #(1);
endAnimation.duration = 1.0;
[colourbutton.titleLabel.layer addAnimation:endAnimation forKey:#"end"];
[colourbutton.titleLabel.layer removeAnimationForKey:#"opacity"];
The key here is to use the presentation layer to get the current state. Don't forget to set the actual end state of the layer, because the animation will be removed on completion.
In NKorotov's answer, he uses the presentationLayer to find out where you are in the animation. That is the correct way to go.
You could go with this solution, although IMO you would also have to calculate the duration animation correctly (based on the duration of the original animation and on how far you are along the animation path currently).
If you find it "silly" to add a new animation, you could perhaps call removeAllAnimations using dispatch_after at the correct time.

Stop spinning image in place, using CABasicAnimation

I have an UIImageView spinning using the following code:
CABasicAnimation *rotation;
rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotation.fromValue = [NSNumber numberWithFloat:0];
rotation.toValue = [NSNumber numberWithFloat:(2*M_PI)];
rotation.duration = 2.0;
rotation.repeatCount = HUGE_VALF;
[myView.layer addAnimation:rotation forKey:#"Spin"];
And I can stop it by running
[myView.layer removeAnimationForKey:#"Spin"];
However, when the animation stops, the image resets to where it started. This looks a little awkward if the image is mid-turn, so I would like for it to stop exactly where it is in the rotation. I assume this would require getting the current rotation amount just before stopping the animation, and then setting it back to that rotation amount instantly after stopping the animation. I do not know how to do this though.
There a couple ways of doing this. One way is my affecting the layers speed property which you can find an example of in the answer to this post: Is there a way to pause a CABasicAnimation?. But this way is more of a means to pause the animation.
Since you want to stop it, I recommend you keep calling removeAnimationForKey: like you are, and coupling it with a CATransform3D made to match the view's presentationLayer's transform before it stopped animating. Give it a try:
CATransform3D myTransform = [(CALayer*)[myView.layer presentationLayer] transform];
[myView.layer removeAnimationForKey:#"Spin"];
[myView.layer setTransform:myTransform];

Trying to delay CABasicAnimation position and opacity of layer by 3 seconds but

I am trying to delay the animation of layer's opacity and position by 3 seconds using setBeginTime. I have called the layer boxLayer. The animation is going well however during the first 3 seconds (the layer is not supposed to show yet) the layer is displayed at its final position and opacity. It should not. Group animation does not resolve the issue. Could anyone help? See code below:
// Create an animation that will change the opacity of a layer
CABasicAnimation *fader = [CABasicAnimation animationWithKeyPath:#"opacity"];
// It will last 1 second and will be delayed by 3 seconds
[fader setDuration:1.0];
[fader setBeginTime:CACurrentMediaTime()+3.0];
// The layer's opacity will start at 0.0 (completely transparent)
[fader setFromValue:[NSNumber numberWithFloat:startOpacity]];
// And the layer will end at 1.0 (completely opaque)
[fader setToValue:[NSNumber numberWithFloat:endOpacity]];
// Add it to the layer
[boxLayer addAnimation:fader forKey:#"BigFade"];
// Maintain opacity to 1.0 JUST TO MAKE SURE IT DOES NOT GO BACK TO ORIGINAL OPACITY
[boxLayer setOpacity:endOpacity];
// Create an animation that will change the position of a layer
CABasicAnimation *mover = [CABasicAnimation animationWithKeyPath:#"position"];
// It will last 1 second and will be delayed by 3 seconds
[mover setDuration:1.0];
[mover setBeginTime:CACurrentMediaTime()+3.0];
// Setting starting position
[mover setFromValue:[NSValue valueWithCGPoint:CGPointMake(startX, startY)]];
// Setting ending position
[mover setToValue:[NSValue valueWithCGPoint:CGPointMake(endX, endY)]];
// Add it to the layer
[boxLayer addAnimation:mover forKey:#"BigMove"];
// Maintain the end position at 400.0 450.0 OTHERWISE IT IS GOING BACK TO ORIGINAL POSITION
[boxLayer setPosition:CGPointMake(endX, endY)];
The problem is that you're setting the boxLayer properties of position and of opacity to their end values. You need to:
Set the boxLayer properties to their starting values, not their ending values (this is why it's starting in the ending position/opacity ... usually if the animation starts immediately, this isn't an issue, but because you're deferring the start, using the ending positions is problematic);
For your two animations, you have to change removedOnCompletion to NO and fillMode to kCAFillModeForwards (this is the correct way to keep it from reverting back to the original position upon completion).
Thus:
// Create an animation that will change the opacity of a layer
CABasicAnimation *fader = [CABasicAnimation animationWithKeyPath:#"opacity"];
// It will last 1 second and will be delayed by 3 seconds
[fader setDuration:1.0];
[fader setBeginTime:CACurrentMediaTime()+3.0];
// The layer's opacity will start at 0.0 (completely transparent)
[fader setFromValue:[NSNumber numberWithFloat:startOpacity]];
// And the layer will end at 1.0 (completely opaque)
[fader setToValue:[NSNumber numberWithFloat:endOpacity]];
// MAKE SURE IT DOESN'T CHANGE OPACITY BACK TO STARTING VALUE
[fader setRemovedOnCompletion:NO];
[fader setFillMode:kCAFillModeForwards];
// Add it to the layer
[boxLayer addAnimation:fader forKey:#"BigFade"];
// SET THE OPACITY TO THE STARTING VALUE
[boxLayer setOpacity:startOpacity];
// Create an animation that will change the position of a layer
CABasicAnimation *mover = [CABasicAnimation animationWithKeyPath:#"position"];
// It will last 1 second and will be delayed by 3 seconds
[mover setDuration:1.0];
[mover setBeginTime:CACurrentMediaTime()+3.0];
// Setting starting position
[mover setFromValue:[NSValue valueWithCGPoint:CGPointMake(startX, startY)]];
// Setting ending position
[mover setToValue:[NSValue valueWithCGPoint:CGPointMake(endX, endY)]];
// MAKE SURE IT DOESN'T MOVE BACK TO STARTING POSITION
[mover setRemovedOnCompletion:NO];
[mover setFillMode:kCAFillModeForwards];
// Add it to the layer
[boxLayer addAnimation:mover forKey:#"BigMove"];
// SET THE POSITION TO THE STARTING POSITION
[boxLayer setPosition:CGPointMake(startX, startY)];
Personally, I think you're doing a lot of work for something that's done far more easily with block-based animation on the view (for the purposes of this demonstration, I'm assuming your boxLayer is a CALayer for a control called box). You don't need Quartz 2D, either, if you do it this way:
box.alpha = startOpacity;
box.frame = CGRectMake(startX, startY, box.frame.size.width, box.frame.size.height);
[UIView animateWithDuration:1.0
delay:3.0
options:0
animations:^{
box.alpha = endOpacity;
box.frame = CGRectMake(endX, endY, box.frame.size.width, box.frame.size.height);
}
completion:nil];
For using beginTime you should make necessary configuration of your animation object and set fillMode to kCAFillModeBackwards like
zoomAnimation.fillMode = kCAFillModeBackwards;
That's said in Apple documentation:
Use the beginTime property to set the start time of an animation. Normally, animations begin during the next update cycle. You can use the beginTime parameter to delay the animation start time by several seconds. The way to chain two animations together is to set the begin time of one animation to match the end time of the other animation.
If you delay the start of an animation, you might also want to set the fillMode property to kCAFillModeBackwards. This fill mode causes the layer to display the animation’s start value, even if the layer object in the layer tree contains a different value. Without this fill mode, you would see a jump to the final value before the animation starts executing. Other fill modes are available too.
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW2
Also, from Rob's answer:
For your two animations, you have to change removedOnCompletion to NO and fillMode to kCAFillModeForwards (this is the correct way to keep it from reverting back to the original position upon completion).
It's a kind of controversial statement, because:
Once the animation is removed, the presentation layer will fall back to the values of the model layer, and since we’ve never modified that layer’s position, our spaceship reappears right where it started.
There are two ways to deal with this issue:
The first approach is to update the property directly on the model layer. This is the recommended approach, since it makes the animation completely optional.
Alternatively, you can tell the animation to remain in its final state by setting its fillMode property to kCAFillModeForwards and prevent it from being automatically removed by setting removedOnCompletion to NO. However, it’s a good practice to keep the model and presentation layers in sync, so this approach should be used carefully.
From https://www.objc.io/issues/12-animations/animations-explained/
This article explains well why you shouldn't use removedOnCompletion with fillMode https://www.objc.io/issues/12-animations/animations-explained/
In my case I'm animating the layer of a view that functions as a navigation but delaying a bounce animation that is inside that view ; I NEED BOTH OF THESE POSITIONS UPDATED ON THE LAYER since it can be dismissed and then shown again. Using removedOnCompletion will not update the layer's value once the animation completes
The way I do it is update the layer in a CATransaction completion block
CATransaction.setCompletionBlock {
// update the layer value
}
CATransaction.begin()
// setup and add your animation
CATransaction.commit()

Resources