Resume CABasicAnimation using negative speed? - ios

I resume my CABasicAnimation with the following code:
CFTimeInterval pausedtime = layer.timeOffset;
layer.speed = 1.0;
layer.timeOffset = 0.0;
layer.beginTime = [layer convertTime:CACurrentMediaTime() toLayer:nil] - pausedTime;
which is pretty straightforward. At the very end of this article the author states that with negative speed value animation reverses. I can't understand what timeOffset and beginTime should look like in this case?
P.S. I am aware that I can reverse animation by obtaining current values from presentation layer and setting toValue and fromValue. I want to figure out if it is possible with speed property.

You will have problems with resuming an animation backwards with .timeOffset = 0. .timeOffset = 0 means that the animation is at its beginning. So, with reverse speed the animation will be finished immediately. Thus, if you, for example, have animation which is removed automatically on completion — it won't play anything: just will disappear.
CAAnimation has poor documentation indeed. And their example of resuming uses .beginTime property only. We can use .timeOffset property instead.
1. Set your layer's begin time to the current time of its container:
// set begin time to current time of superlayer to start immediately
let sTime = layer.superlayer?.convertTime(CACurrentMediaTime(), to: nil) ?? 0
layer.beginTime = sTime
2. Go to the moment of animation, where you want to resume, using .timeOffset:
// set starting point of animation to the current "progress"
layer.timeOffset = progress
Where progress is the time position you want to resume from in terms of animation duration. i.e. if you have an animation duration of 0.6 — then 0 ≤ progress ≤ 0.6.
3. Launch animation in reverse direction:
// set negative speed to have reverse animation
layer.speed = -1.0
Don't forget to remove animation on its completion, if needed.
This is the picture of how these settings are applied in fact:

Related

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

Core Animation: Pause before autoreversing?

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.

Starting animation at a given offset

Is there a way to achieve what the timeOffset property of an animation does, starting the animation at some point in time, but without the "wrapping" behaviour?
I have two animations (in two different layers):
// This is the animation of the stroke of a circle
let progressAnim = CABasicAnimation(keyPath: "strokeEnd")
progressAnim.duration = duration
progressAnim.fromValue = 0
progressAnim.toValue = 1
progressAnim.fillMode = kCAFillModeBackwards
progressAnim.timeOffset = elapsed
// This is the animation of a pointer that follow the same circle as above
let arrowAnim = CAKeyframeAnimation(keyPath: "position")
arrowAnim.duration = duration
arrowAnim.rotationMode = kCAAnimationRotateAuto
arrowAnim.path = arrowPath.CGPath
arrowAnim.fillMode = kCAFillModeBackwards
arrowAnim.timeOffset = elapsed
This starts the animations at the wanted progress, but when it reaches what is supposed to the the end, it starts over and lasts for the remaining duration. I realize this is how timeOffset is specified to work, but I was hoping that it is possible to somehow achieve the same without the wrap.
Instead of the timeOffset hack, you can calculate the proper fromValue and corresponding duration.
e.g.
progressAnim.duration = duration - elapsed
progressAnim.fromValue = elapsed / duration
Yes, it's possible. You'll need to use the same settings that you use to pause and resume an "in-flight" animation. The documentation is pretty weak, and the properties of the animation are confusing as hell. Every time I have to work with pausing and resuming animation I have to spend like half a day figuring it out again, and then a week later, I've forgotten how to do it again. It's been at least 6 months since I've dealt with it, so I really forget how to do it.
I have a demo project on github that shows how to pause and resume an animation, and even to use a slider to move an animation forward and backward. I suggest you take a look at that. It has all the pieces you need:
Keyframe view animation demo on Github
I think I figured it out. By trial and error, it seems to work when I set beginTime of the animation to a time in the past, like this:
let beginTime = CACurrentMediaTime() - offset
// Set the beginTime of the progress animation
progressAnim.beginTime = beginTime
// Set the begintTime of the arrow animation
arrowAnim.beginTime = beginTime
This seems to have the desired effect.

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

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