CoreAnimation automatically restarts when iterationStart is set - dart

I want to skip the first part of an animation and jump right to let's say 3/4 of the duration. For this purpose I build the following script in Dart.
Element target = getTarget(layer.element);
CoreAnimation animation = new CoreAnimation();
animation.target = target;
animation.duration = 2000;
animation.iterationStart = 0.75; // 1500ms
animation.keyframes = [
{'transform': 'translate(0px, 0px)'},
{'transform': 'translate(20px, 50px)'}
];
// I also tried to add the following attributes
animation.iterations = 1; // doesnt work
animation.iterationCount = 1; // doesnt work
animation.play();
The animation starts at the correct position, but once it has reached its last keyframe {'transform': 'translate(20px, 50px)'} it jumps back to the first keyframe and play the complete animation again. What for gods sake is happening here? Am I missing something?
Note: I already tried to set iteration count to 1.

Try to set animation.iterations to 1.

Related

Is it possible to pause a CAEmitterLayer?

I have a CAEmitterLayer instance that emits some CAEmitterCells. I'm wondering, is it possible to pause this layer such that no new CAEmitterCells are produced and the ones that have been produced remained fixed in their position on the screen? Then, when the CAEmitterLayer instance is "un-paused", the fixed CAEmitterCells on the screen start to move again.
Thanks for any help here.
EDIT
Setting:
emitterLayer.speed = 0.1
where emitterLayer is an instance of a subclass of CAEmitterLayer, just removes the layer completely from the view.
Setting:
emitterLayer.lifetime = 0.0
just stops any new emitterCells being produced but doesn't "freeze" the existing emitterCells at the current position.
You can set the lifetime property of the CAEmitterLayer to 0, which will cause newly emitted cells to not even be rendered, but will leave already existing cells unaffected. When you want to "un-pause" your emitter layer, you can simply reset lifetime to whatever it was before the pause.
To freeze the existing cells as well, you can set speed to 0 and also add a timeOffset.
extension CAEmitterLayer {
func pause() {
// Freeze existing cells
self.speed = 0
self.timeOffset = convertTime(CACurrentMediaTime(), from: self)
// Stop creating new cells
self.lifetime = 0
}
}
Then you can simply call it like emitterLayer.pause()

Animate a UIView along a part of a bezier path

I'm trying animate a UIView along a portion of a bezier path. I found a way to move the the view to any part of the path using this code:
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = trackPath.cgPath
animation.rotationMode = kCAAnimationRotateAuto
animation.speed = 0
animation.timeOffset = offset
animation.duration = 1
animation.calculationMode = kCAAnimationPaced
square.layer.add(animation, forKey: "animate position along path")
However, this just moves the view to the desired point and doesn't animate it. How do you animate a view along over a portion of a bezier path?
Thanks
You can accomplish this by modifying the timing of the "complete" animation and a animation group that wraps it.
To illustrate how the timing of such an animation works, imagine – instead of animating a position along a path – that a color is being animated. Then, the complete animation from one color to another can be illustrated as this, where a value further to the right is at a later time — so that the very left is the start value and the very right is the end value:
Note that the "timing" of this animation is linear. This is intentional since the "timing" of the end result will be configured later.
In this animation, imagine that we're looking to animate only the middle third, this part of the animation:
There are a few steps to animating only this part of the animation.
First, configure the "complete" animation to have linear timing (or in the case of an animation along a path; to have a paced calculation mode) and to have a the "complete" duration.
For example: if you're looking to animate a third of the animation an have that take 1 second, configure the complete animation to take 3 seconds.
let relativeStart = 1.0/3.0
let relativeEnd = 2.0/3.0
let duration = 1.0
let innerDuration = duration / (relativeEnd - relativeStart) // 3 seconds
// configure the "complete" animation
animation.duration = innerDuration
This means that the animation currently is illustrated like this (the full animation):
Next, so that the animation "starts" a third of the way into the full animation, we "offset" its time by a third of the duration:
animation.timeOffset = relativeStart * innerDuration
Now the animation is illustrated like this, offset and wrapping from its end value to its start value:
Next, so that we only display part of this animation, we create an animation group with the wanted duration and add only the "complete" animation to it.
let group = CAAnimationGroup()
group.animations = [animation]
group.duration = duration
Even though this group contains an animation that is 3 seconds long it will end after just 1 second, meaning that the 2 seconds of the offset "complete" animation will never be shown.
Now the group animation is illustrated like this, ending after a third of the "complete" animation:
If you look closely you'll see that this (the part that isn't faded out) is the middle third of the "complete" animation.
Now that this group animates animates between the wanted values, it (the group) can be configured further and then added to a layer. For example, if you wanted this "partial" animation to reverse, repeat, and have a timing curve you would configure these things on the group:
group.repeatCount = HUGE
group.autoreverses = true
group.timingFunction = CAMediaTimingFunction(name: "easeInEaseOut")
With this additional configuration, the animation group would be illustrated like this:
As a more concrete example, this is an animation that I created using this technique (in fact all the code is from that example) that moves a layer back and forth like a pendulum. In this case the "complete" animation was a "position" animation along a path that was a full circle
I've done a similar animation just a few days ago:
// I want the animation to go along the circular path of the oval shape
let flightAnimation = CAKeyframeAnimation(keyPath: "position")
flightAnimation.path = ovalShapeLayer.path
// I set this one to make the animation go smoothly along the path
flightAnimation.calculationMode = kCAAnimationPaced
flightAnimation.duration = 1.5
flightAnimation.rotationMode = kCAAnimationRotateAuto
airplaneLayer.add(flightAnimation, forKey: nil)
I see that you set speed to zero and a time offset. Why do you need them?
I would suggest to try the animation using just the parameters in the above code and then try to tune it from them.

CAEmitterLayer doesn't add basic animation if beginTime was set for it

I am trying to animate an explosion with CAEmitterLayer and a couple of CAEmitterCell. This should happen after a short delay after user sees a view. I start my animation in viewDidAppear.
Particle animation itself works fine, but as in this question Initial particles from CAEmitterLayer don't start at emitterPosition unless I set emitterLayer.beginTime = CACurrentMediaTime() animation appears to user as one that has been running for some time.
Now to actually achieve an explosion I have to stop emitting particles at some point. I try to use this code to setup CABasicAnimation which would stop emitter after some time:
// emitter layer is reused (remove all animations, remove all cells, remove from superlayer)
... // emitter layer setup in a function "explode" which is called from viewDidAppear
emitterLayer.beginTime = CACurrentMediaTime()
let birthRateAnimation = CABasicAnimation(keyPath: "birthRate")
birthRateAnimation.toValue = 0
birthRateAnimation.timingFunction = CAMediaTimingFunction.init(name:kCAMediaTimingFunctionEaseOut)
birthRateAnimation.beginTime = CACurrentMediaTime() + 1
birthRateAnimation.duration = 5
birthRateAnimation.delegate = self // (self is view controller)
birthRateAnimation.setValue("expl", forKey: "animName")
emitterLayer.add(birthRateAnimation, forKey: "birthRateAnimation")
self.view.layer.addSublayer(emitterLayer)
So now with this code birthRateAnimation is not actually triggered. I have logs in animationDidStop and animationDidStart which don't print anything.
Now, if I call explode function on button tap I see no particle animation at all, but in logs I see "animation start" / "animation stop" messages.
Any idea why?
I found two problems that kept the animation from working correctly.
The animation is on birthRate, but CAEmitterLayer does not have a birthRate property; CAEmitterCell does, though. Give your CAEmitterCell a name and change CABasicAnimation(keyPath: "birthRate") to CABasicAnimation(keyPath: "emitterCells.cellName.birthRate")
Setting the beginTime to CACurrentMediaTime() + 1.0 does not do what you want, because the emitter layer has its own time scale. Changing that to emitterLayer.convertTime(CACurrentMediaTime(), from: nil) + 1.0 does what you want.
Late to the party, but I ran into the very same thing and the solution is to set beginTime of your emitter to CACurrentMediaTime(), e.g.:
let emitter = makeAwesomeEmitter()
emitter.beginTime = CACurrentMediaTime()
let emitterAnimation = makeAwesomeEmitterBirthRateAnimation()
emitterAnimation.beginTime = 1.0 // 1 sec delay
emitter.add(emitterAnimation, forKey: nil)
myView.layer.addSublayer(emitter)
Rationale is that the emitter pre-generates particles to imitate it running already for a while. See also this SO question which brought me to the solution.

How to synchronize another object with a repeating animation?

I have an animation involving an object continuously bouncing back and forth between two walls with a time period of 2 seconds between two positions given by the CGPoints 'positionStart' and 'positionEnd'. The code doing this is a pretty simple and basic animation:
CABasicAnimation *theAnimation;
theAnimation=[CABasicAnimation animationWithKeyPath:#"position"];
theAnimation.duration=1.0;
theAnimation.beginTime=CACurrentMediaTime()+1;
theAnimation.repeatCount=HUGE_VALF;
theAnimation.autoreverses=YES;
theAnimation.fromValue=[NSValue valueWithCGPoint:positionStart];
theAnimation.toValue=[NSValue valueWithCGPoint:positionEnd];
[self.pulseLayer addAnimation:theAnimation forKey:#"animatePosition"];
Now here's the question: I want to have this section of code send a message to another object with every "tick" of this clock; that is, every time pulseLayer's presented position goes to 'positionStart' I want this section of code to send a target-action message to another object (let's call it Clock #2) which I want to keep in sync with the periodic bouncing of the 'pulseLayer'.
Is there any simple way of doing this? The best alternative ideas I could come up with are (1) to start an NSTimer at the same time as the start of this animation to send "tick" timing pulses to Clock #2, but I worry that although the NSTimer object and the 'pulseLayer' bounce may start off in sync that small timing errors may build up over time so that they will no longer appear to be in sync after a long time. There would certainly be nothing to force them to remain synchronized. Another idea (2) is to do away with this endless bouncing of pulseLayer (i.e., change theAnimation.repeatCount to equal 0) and have another object send timing pulses to start the one-bounce animation every 2 seconds to both this object and to the "Clock #2" object that I want to keep in sync.
Any ideas about the best way to implement what I want to do here?
autoreverse does not send notification
(until all animations completed, which is never in the example above)
Use 2 different animations. It is simple to setup:
Setup:
// To
self.toAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
self.toAnimation.duration=1.0;
self.toAnimation.fromValue=[NSValue valueWithCGPoint:positionStart];
self.toAnimation.toValue=[NSValue valueWithCGPoint:positionEnd];
self.toAnimation.delegate:self;
// And fro
self.froAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
self.froAnimation.duration=1.0;
self.froAnimation.fromValue=[NSValue valueWithCGPoint:positionEnd];
self.froAnimation.toValue=[NSValue valueWithCGPoint:positionStart];
self.froAnimation.delegate:self;
Start:
// Start the chain of animations by adding the "next" (the first) animation
[self toAndFro:self.froAnimation];
Delegate:
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished {
[self toAndFro:(CABasicAnimation*)animation];
}
Core:
- (void)toAndFro:(CABasicAnimation *)stoppedAnimation {
BOOL wasFro = self.froAnimation == stoppedAnimation;
CABasicAnimation * theAnimation = ((wasFro)
? self.toAnimation
: self.froAnimation);
[self.pulseLayer addAnimation:theAnimation forKey:#"animatePosition"];
// Tick here! You are now in perfect sync
}

Create a spinning animation

I have an image that I want to rotate 360° (clockwise) and then repeat and I'm having trouble hitting on the right way to do this. I can do this:
UIView.Animate(1, 0, UIViewAnimationOptions.Repeat,
() =>
{
LoadingImage.Transform = MonoTouch.CoreGraphics.CGAffineTransform.MakeRotation(-(float)(Math.PI));
},
() =>
{
});
Which rotates my image 180° and then snaps back to the start and repeats. So, I thought if I do something like 1.99 * PI, it would rotate almost all the way around and probably look ok. Unfortunately, the animation system is smart than that and will just rotate in the opposite direction instead!
So what's the right way to get an image to spin 360° continuously?
Update
With the help or Eric's comment, I've got to this:
CABasicAnimation rotationAnimation = new CABasicAnimation();
rotationAnimation.KeyPath = "transform.rotation.z";
rotationAnimation.To = new NSNumber(Math.PI * 2);
rotationAnimation.Duration = 1;
rotationAnimation.Cumulative = true;
rotationAnimation.RepeatCount = 10;
LoadingImage.Layer.AddAnimation(rotationAnimation, "rotationAnimation");
Which does spin 360­­°, 10 times in my example (see RepeatCount), but I don't see a way to tell it to repeat until I stop it. CABasicAnimation has a RepeatCount and a RepeatDuration, but doesn't seem to have a property to tell it to just keep repeating. I can set RepeatCount to some suitably high value that it'll keep spinning until the user has probably lost interest, but that doesn't seem like a great solution.
Looks like the solution adapted from the link Eric provided looks like the best choice:
CABasicAnimation rotationAnimation = new CABasicAnimation();
rotationAnimation.KeyPath = "transform.rotation.z";
rotationAnimation.To = new NSNumber(Math.PI * 2);
rotationAnimation.Duration = 1;
rotationAnimation.Cumulative = true;
rotationAnimation.RepeatCount = float.MaxValue;
LoadingImage.Layer.AddAnimation(rotationAnimation, "rotationAnimation");
Set RepeatCount to float.MaxValue to effectively keep it repeating forever (why a count is a float rather than an int remains a mystery).
Try animation.repeatCount = HUGE_VALF;
I think the reason repeat count is a float is to allow for partial cycles of an animation. Maybe you want a circle to animate by pulsing, but stop at the opposite end of where you started.

Resources