I have an animation on a CALayer which repeats indefinitely however I want to change it such that the animation is triggered by a certain timing event.
I can get it to work by removing and re-adding the animation event when the trigger occurs but this seems a bit kludgy, is there an alternative way without having to constantly remove and add the animation all the time?
Here's the code in sketch form:
- (void) init
{
….
CALayer *sublayer = …
[self.layer addSublayer:sublayer];
[self createAnimationGroup];
[sublayer addAnimation: self.animationGroup forKey:#"MyKey"];
}
- (void) createAnimationGroup
{
CAMediaTimingFunction *defaultCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
self.animationGroup = [CAAnimationGroup animation];
self.animationGroup.repeatCount = 0.0f; // was previously INFINITY;
<snip>
NSArray *animations = #[scaleAnimation, opacityAnimation];
self.animationGroup.animations = animations;
}
- (void) onTrigger
{
[self.layer.sublayers[0] removeAnimationForKey:#"MyKey"];
[self.layer.sublayers[0] addAnimation:self.animationGroup forKey:#"MyKey"];
}
My question is is the way I have implemented onTrigger ok, it works, but is there a way of triggering the animation directly rather than indirectly via removing and adding it?
Take a look at this suggestion from Apple. Specifically pausing and resuming an animation.
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW15
Related
Here's some relevant code inside a UIView subclass:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
- (CGFloat) currentX {
CALayer* presLayer = self.layer.presentationLayer;
return presLayer.position.x;
}
When I use [self currentX] while the animation is running, I get 200 (the end value) rather than a value between 0 (the start value) and 200. And yes, the animation is visible to the user, so I'm really confused here.
Here's the code where I call doMyCoolAnimation:, as well as currentX after 1 second.
[self doMyCoolAnimation];
CGFloat delay = 1; // 1 second delay
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(#"%f", [self currentX]);
});
Any ideas?
I don't know where the idea for using KVC setters in animation code came from, but that's what the animation itself is for. You're basically telling the layer tree to immediately update to the new position with this line:
[self.layer setValue:#200 forKeyPath:anim.keyPath];
Then wondering why the layer tree won't animate to that position with an animation that has no starting or ending values. There's nothing to animate! Set the animation's toValue and fromValue as appropriate and ditch the setter. Or, if you wish to use an implicit animation, keep the setter, but ditch the animation and set its duration by altering the layer's speed.
My UIView's layer's presentationLayer was not giving me the current values. It was instead giving me the end values of my animation.
To fix this, all I had to do was add...
anim.fromValue = [self.layer valueForKeyPath:#"position.x"];
...to my doMyCoolAnimation method BEFORE I set the end value with:
[self.layer setValue:#200 forKeyPath:#"position.x"];
So in the end, doMyCoolAnimation looks like this:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
anim.fromValue = [self.layer valueForKeyPath:anim.keyPath];
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
The way you are creating your animation is wrong, as CodaFi says.
Either use an explicit animation, using a CABasicAnimation, or use implicit animation by changing the layer's properties directly and NOT using a CAAnimation object. Don't mix the two.
When you create a CABasicAnimation object, you use setFromValue and/or setToValue on the animation. Then the animation object takes care of animating the property in the presentation layer.
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.
I've made a wrapper for compact creation of CABasicAnimation instances.
It's implemented through a category for UIView as an instance method named change:from:to:in:ease:delay:done:. So for example, I can do:
[self.logo
change:#"y"
from:nil // Use current self.logo.layer.position.y
to:#80
in:1 // Finish in 1000 ms
ease:#"easeOutQuad" // A selector for a CAMediaTimingFunction category method
delay:0
done:nil];
The problem
When the CABasicAnimation starts, animationDidStart: handles setting self.logo.layer.position.y to 80 (the end value). Before it worked like this, I tried using animationDidStop:finished: to do the same thing, but found the layer flickering after completing the animation. Now, the layer goes straight to the end value, and no interpolation occurs. I implemented animationDidStart: in my UIView category like so:
- (void)animationDidStart:(CAAnimation *)animation
{
[self.layer
setValue:[animation valueForKey:#"toValue"]
forKeyPath:[animation valueForKey:#"keyPath"]];
}
I'm setting the end value in order to match the model layer with the presentation layer (in other words, to prevent resetting back to the start position).
Here's the implementation to change:from:to:in:ease:delay:done:...
- (CABasicAnimation*) change:(NSString*)propertyPath
from:(id)from
to:(id)to
in:(CGFloat)seconds
ease:(NSString*)easeName
delay:(CGFloat)delay
done:(OnDoneCallback)done
{
NSString* keyPath = [app.CALayerAnimationKeyPaths objectForKey:propertyPath];
if (keyPath == nil) keyPath = propertyPath;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath];
if (delay > 0) animation.beginTime = CACurrentMediaTime() + delay;
if (easeName != nil) animation.timingFunction = [CAMediaTimingFunction performSelector:NSSelectorFromString(easeName)];
if (from != nil) animation.fromValue = from;
animation.toValue = to;
animation.duration = seconds;
animation.delegate = self;
[self.layer setValue:done forKey:#"onDone"];
[self.layer addAnimation:animation forKey:keyPath];
return animation;
}
In the first line of the above code, this is the NSDictionary I use to convert property shortcuts to the real keyPath. This is so I can just type #"y" instead of #"position.y" every time.
app.CALayerAnimationKeyPaths = #{
#"scale": #"transform.scale",
#"y": #"position.y",
#"x": #"position.x",
#"width": #"frame.size.width",
#"height": #"frame.size.height",
#"alpha": #"opacity",
#"rotate": #"transform.rotation"
};
Any questions?
What you're seeing is, I think, expected behavior. You are setting an animatable property after animation of that property has started. That is in fact the most common and recommended way to do exactly what you are seeing happen, i.e. cancel the animation and jump right to the final position now. So you are deliberately canceling your own animation the minute it gets going.
If that's not what you want, don't do that. Just set the animated property to its final value before the animation is attached to the layer - being careful, of course, not to trigger implicit animation as you do so.
I am creating some animation on my application and the code below zooms out an object till it disappears. I can't figure out how to make the object to disappear and keep that way, ie. how to make the animation stay put after it finishes. Any gotchas on that? Cheers!
CABasicAnimation* zoomOut = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
zoomOut.duration = 1;
zoomOut.toValue = [NSNumber numberWithFloat:0];
[draggedObject addAnimation:zoomOut forKey:nil];
I found it. It also needs the two methods below:
zoomOut.removedOnCompletion = NO;
zoomOut.fillMode = kCAFillModeForwards;
Ok so this happens because the animation doesn't actually change the underlying property, which is why it jumps back after the animation is complete.
Try adding this line before the line starting the animation -
zoomOut.removedOnCompletion = NO;
I have a feeling I'm overlooking something elementary, but what better way to find it than to be wrong on the internet?
I have a fairly basic UI. The view for my UIViewController is a subclass whose +layerClass is CAGradientLayer. Depending on the user's actions, I need to move some UI elements around, and change the values of the background's gradient. The code looks something like this:
[UIView animateWithDuration:0.3 animations:^{
self.subview1.frame = CGRectMake(...);
self.subview2.frame = CGRectMake(...);
self.subview2.alpha = 0;
NSArray* newColors = [NSArray arrayWithObjects:
(id)firstColor.CGColor,
(id)secondColor.CGColor,
nil];
[(CAGradientLayer *)self.layer setColors:newColors];
}];
The issue is that the changes I make in this block to the subviews animate just fine (stuff moves and fades), but the change to the gradient's colors does not. It just swaps.
Now, the documentation does say that Core Animation code within an animation block won't inherit the block's properties (duration, easing, etc.). But is it the case that that doesn't define an animation transaction at all? (The implication of the docs seems to be that you'll get a default animation, where I get none.)
Do I have to use explicit CAAnimation to make this work? (And if so, why?)
There seem to be two things going on here. The first (as Travis correctly points out, and the documentation states) is that UIKit animations don't seem to hold any sway over the implicit animation applied to CALayer property changes. I think this is weird (UIKit must be using Core Animation), but it is what it is.
Here's a (possibly very dumb?) workaround for that problem:
NSTimeInterval duration = 2.0; // slow things down for ease of debugging
[UIView animateWithDuration:duration animations:^{
[CATransaction begin];
[CATransaction setAnimationDuration:duration];
// ... do stuff to things here ...
[CATransaction commit];
}];
The other key is that this gradient layer is my view's layer. That means that my view is the layer's delegate (where, if the gradient layer was just a sublayer, it wouldn't have a delegate). And the UIView implementation of -actionForLayer:forKey: returns NSNull for the "colors" event. (Probably every event that isn't on a specific list of UIView animations.)
Adding the following code to my view will cause the color change to be animated as expected:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
id<CAAction> action = [super actionForLayer:layer forKey:event];
if( [#"colors" isEqualToString:event]
&& (nil == action || (id)[NSNull null] == action) ) {
action = [CABasicAnimation animationWithKeyPath:event];
}
return action;
}
You have to use explicit CAAnimations, because you're changing the value of a CALayer.
UIViewAnimations work on UIView properties, but not directly on their CALayer's properties...
Actually, you should use a CABasicAnimation so that you can access its fromValue and toValue properties.
The following code should work for you:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[UIView animateWithDuration:2.0f
delay:0.0f
options:UIViewAnimationCurveEaseInOut
animations:^{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"colors"];
animation.duration = 2.0f;
animation.delegate = self;
animation.fromValue = ((CAGradientLayer *)self.layer).colors;
animation.toValue = [NSArray arrayWithObjects:(id)[UIColor blackColor].CGColor,(id)[UIColor whiteColor].CGColor,nil];
[self.layer addAnimation:animation forKey:#"animateColors"];
}
completion:nil];
}
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
NSString *keyPath = ((CAPropertyAnimation *)anim).keyPath;
if ([keyPath isEqualToString:#"colors"]) {
((CAGradientLayer *)self.layer).colors = ((CABasicAnimation *)anim).toValue;
}
}
There is a trick with CAAnimations in that you HAVE to explicitly set the value of the property AFTER you complete the animation.
You do this by setting the delegate, in this case I set it to the object which calls the animation, and then override its animationDidStop:finished: method to include the setting of the CAGradientLayer's colors to their final value.
You'll also have to do a bit of casting in the animationDidStop: method, to access the properties of the animation.