Animate drawing the fill of a CAShapeLayer - ios

I've been playing around with drawing a path using a CAShapeLayer as outlined in this great article, http://oleb.net/blog/2010/12/animating-drawing-of-cgpath-with-cashapelayer, but I'm wondering if there's a way to animate the filling of a layer.
For example, I have some text I want to draw on the screen, but I've only been able to draw the stroke of the text and not the fill. Another example, I have a star shape that I would like to animate it being filled in.
Is this possible using a CAShapeLayer or other object?
Thanks!

Its most of the time the same code, you just have to set different values for fromValue and toValue of your CABasicAnimation. I created a category which returns me a CABasicAnimation:
Animation for StrokeEnd
+ (CABasicAnimation *)animStrokeEndWithDuration:(CGFloat)dur
delegate:(id)target{
CABasicAnimation *animLine =
[CABasicAnimation animationWithKeyPath:#"strokeEnd"];
[animLine setDuration:dur];
[animLine setFromValue:[NSNumber numberWithFloat:0.0f]];
[animLine setToValue:[NSNumber numberWithFloat:1.0f]];
[animLine setRemovedOnCompletion:NO];
[animLine setFillMode:kCAFillModeBoth];
[animLine setDelegate:target];
[animLine setTimingFunction:
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
return animLine;
}
Animation for fillColor
+ (CABasicAnimation *)animFillColorWithDur:(CGFloat)dur
startCol:(UIColor *)start
endColor:(UIColor *)end
delegate:(id)target{
CABasicAnimation *animFill =
[CABasicAnimation animationWithKeyPath:#"fillColor"];
[animFill setDuration:dur];
[animFill setFromValue:(id)start.CGColor];
[animFill setToValue:(id)end.CGColor];
[animFill setRemovedOnCompletion:NO];
[animFill setDelegate:target];
[animFill setFillMode:kCAFillModeBoth];
return animFill;
}
The returned CABasicAnimation just has to be added to a CAShapeLayer:
[_myShapeLayer addAnimation:returnedAnimation forKey:#"animKey"]

Yes, it is possible.
CAShapeLayers have a fillColor property which is animatable.
It works the same way as changing the strokeEnd / strokeStart like you've already done with with your animation.

Related

CALayer position animation on video layer

In this tutorial I have found how to make an animation CALayer in video:
http://www.raywenderlich.com/30200/avfoundation-tutorial-adding-overlays-and-animations-to-videos
CABasicAnimation *animation
=[CABasicAnimation animationWithKeyPath:#"opacity"];
animation.duration=3.0;
animation.repeatCount=5;
animation.autoreverses=YES;
// animate from fully visible to invisible
animation.fromValue=[NSNumber numberWithFloat:1.0];
animation.toValue=[NSNumber numberWithFloat:0.0];
animation.beginTime = AVCoreAnimationBeginTimeAtZero;
[overlayLayer1 addAnimation:animation forKey:#"animateOpacity"];
But it does not work when I want to animate the movement of CALayer:
animation.keyPath = #"position.x";
or
animation.keyPath = #"position.y";
Is it possible to animate the movement of the CALayer?
Any animations will be interpreted on the video's timeline, not real-time, so you should:
Set animations’ beginTime property to AVCoreAnimationBeginTimeAtZero rather than 0 (which CoreAnimation replaces with CACurrentMediaTime);
Set removedOnCompletion to NO on animations so they are not automatically removed;
Try using this keypath :
animation.keyPath = #"transform.translation.x";

CoreAnimation - CABasicAnimation get value steps for 30-60 FPS animations

I've got a CABasicAnimation that is operating on the transform property of a CALayer.
The animation looks fairly cool and it all works fine, but now I want to know through which values the transform of the layer is going to go through for a perfect 30/60 FPS animation.
I don't want to run the aniamtion and measure the values, I want to know beforehand.
The timingFunction has to be taken into account for this calculation.
How can I find out these transform values?
That's the animation code:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath: #"transform"];
animation.fromValue = [NSValue valueWithCATransform3D: _myLayer.transform];
animation.toValue = valueTransform;
animation.duration = 0.7;
animation.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut];
[_myLayer addAnimation: animation forKey:#"transformAnimation"];

CABasicAnimation: What if CAShapeLayer position not changed?

I'm really new on the CABasicAnimation topic. If you want to animate the x
position of an CAShapeLayer from the current x position to a specific x position you should
use the following:
NSLog(#"CABasicAnimation now...");
CABasicAnimation *layerPositionAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
[layerPositionAnimation setDuration:0.330];
[layerPositionAnimation setRemovedOnCompletion: NO];
[layerPositionAnimation setFillMode: kCAFillModeForwards];
[layerPositionAnimation setTimingFunction: [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[layerPositionAnimation setFromValue:[NSValue valueWithCGPoint:CGPointMake(self.shapeLayer.position.x, self.shapeLayer.position.y)]];
[layerPositionAnimation setToValue:[NSValue valueWithCGPoint:CGPointMake(14.0, self.shapeLayer.position.y)]];
[self.shapeLayer addAnimation:layerPositionAnimation forKey:#"layerPositionAnimation"];
In my case the self.shapeLayer is a CAShapeLayer used as a simple mask:
[[overlayTopShadow layer] setMask:self.shapeLayer];
But what if the animation never happens (x position not changed)? What i'm doing wrong in my code?
Any suggestions, ideas, examples?
Thanks a lot

how to use set layer value automatically when use explicit animation?

I can animately move a layer like this:
CABasicAnimation *animation = [CABasicAnimation animation];
[animation setFromValue:[NSValue valueWithCGPoint:self.layer.position]];
[animation setToValue:[NSValue valueWithCGPoint:CGPointMake(100, 100)]];
[self.layer addAnimation:animation forKey:#"position"];
self.layer.position = CGPointMake(100, 100);
however, I have to set the layer.position after the end of animation, is there a way to set it automatically when using explicit animation?
According to the Apple documentation, you have to set it manually :
https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CoreAnimation_guide/CreatingBasicAnimations/CreatingBasicAnimations.html#//apple_ref/doc/uid/TP40004514-CH3-SW3
CABasicAnimation* fadeAnim = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeAnim.fromValue = [NSNumber numberWithFloat:1.0];
fadeAnim.toValue = [NSNumber numberWithFloat:0.0];
fadeAnim.duration = 1.0;
[theLayer addAnimation:fadeAnim forKey:#"opacity"];
// Change the actual data value in the layer to the final value.
theLayer.opacity = 0.0;
Unlike an implicit animation, which updates the layer object’s data value, an explicit animation does not modify the data in the layer tree. Explicit animations only produce the animations. At the end of the animation, Core Animation removes the animation object from the layer and redraws the layer using its current data values. If you want the changes from an explicit animation to be permanent, you must also update the layer’s property as shown in the preceding example.

Cannot get current position of CALayer during animation

I am trying to achieve an animation that when you hold down a button it animates a block down, and when you release, it animates it back up to the original position, but I cannot obtain the current position of the animating block no matter what. Here is my code:
-(IBAction)moveDown:(id)sender{
CGRect position = [[container.layer presentationLayer] frame];
[movePath moveToPoint:CGPointMake(container.frame.origin.x, position.y)];
[movePath addLineToPoint:CGPointMake(container.frame.origin.x, 310)];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = NO;
moveAnim.fillMode = kCAFillModeForwards;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, nil];
animGroup.duration = 2.0;
animGroup.removedOnCompletion = NO;
animGroup.fillMode = kCAFillModeForwards;
[container.layer addAnimation:animGroup forKey:nil];
}
-(IBAction)moveUp:(id)sender{
CGRect position = [[container.layer presentationLayer] frame];
UIBezierPath *movePath = [UIBezierPath bezierPath];
[movePath moveToPoint:CGPointMake(container.frame.origin.x, position.y)];
[movePath addLineToPoint:CGPointMake(container.frame.origin.x, 115)];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = NO;
moveAnim.fillMode = kCAFillModeForwards;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, nil];
animGroup.duration = 2.0;
animGroup.removedOnCompletion = NO;
animGroup.fillMode = kCAFillModeForwards;
[container.layer addAnimation:animGroup forKey:nil];
}
But the line
CGRect position = [[container.layer presentationLayer] frame];
is only returning the destination position not the current position. I need to basically give me the current position of the container thats animating once I release the button, so I can perform the next animation. What I have now does not work.
I haven't analyzed your code enough to be 100% sure why [[container.layer presentationLayer] frame] might not return what you expect. But I see several problems.
One obvious problem is that moveDown: doesn't declare movePath. If movePath is an instance variable, you probably want to clear it or create a new instance each time moveDown: is called, but I don't see you doing that.
A less obvious problem is that (judging from your use of removedOnCompletion and fillMode, in spite of your use of presentationLayer) you apparently don't understand how Core Animation works. This turns out to be surprisingly common, so forgive me if I'm wrong. Anyway, read on, because I will explain how Core Animation works and then how to fix your problem.
In Core Animation, the layer object you normally work with is a model layer. When you attach an animation to a layer, Core Animation creates a copy of the model layer, called the presentation layer, and the animation changes the properties of the presentation layer over time. An animation never changes the properties of the model layer.
When the animation ends, and (by default) is removed, the presentation layer is destroyed and the values of the model layer's properties take effect again. So the layer on screen appears to “snap back” to its original position/color/whatever.
A common, but wrong way to fix this is to set the animation's removedOnCompletion to NO and its fillMode to kCAFillModeForwards. When you do this, the presentation layer hangs around, so there's no “snap back” on screen. The problem is that now you have the presentation layer hanging around with different values than the model layer. If you ask the model layer (or the view that owns it) for the value of the animated property, you'll get a value that's different than what's on screen. And if you try to animate the property again, the animation will probably start from the wrong place.
To animate a layer property and make it “stick”, you need to change the model layer's property value, and then apply the animation. That way, when the animation is removed, and the presentation layer goes away, the layer on screen will look exactly the same, because the model layer has the same property values as its presentation layer had when the animation ended.
Now, I don't know why you're using a keyframe to animate straight-line motion, or why you're using an animation group. Neither seems necessary here. And your two methods are virtually identical, so let's factor out the common code:
- (void)animateLayer:(CALayer *)layer toY:(CGFloat)y {
CGPoint fromValue = [layer.presentationLayer position];
CGPoint toValue = CGPointMake(fromValue.x, y);
layer.position = toValue;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:fromValue];
animation.toValue = [NSValue valueWithCGPoint:toValue];
animation.duration = 2;
[layer addAnimation:animation forKey:animation.keyPath];
}
Notice that we're giving the animation a key when I add it to the layer. Since we use the same key every time, each new animation will replace (remove) the prior animation if the prior animation hasn't finished yet.
Of course, as soon as you play with this, you'll find that if you moveUp: when the moveDown: is only half finished, the moveUp: animation will appear to be at half speed because it still has a duration of 2 seconds but only half as far to travel. We should really compute the duration based on the distance to be travelled:
- (void)animateLayer:(CALayer *)layer toY:(CGFloat)y withBaseY:(CGFloat)baseY {
CGPoint fromValue = [layer.presentationLayer position];
CGPoint toValue = CGPointMake(fromValue.x, y);
layer.position = toValue;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:fromValue];
animation.toValue = [NSValue valueWithCGPoint:toValue];
animation.duration = 2.0 * (toValue.y - fromValue.y) / (y - baseY);
[layer addAnimation:animation forKey:animation.keyPath];
}
If you really need it to be a keypath animation in an animation group, your question should show us why you need those things. Anyway, it works with those things too:
- (void)animateLayer:(CALayer *)layer toY:(CGFloat)y withBaseY:(CGFloat)baseY {
CGPoint fromValue = [layer.presentationLayer position];
CGPoint toValue = CGPointMake(fromValue.x, y);
layer.position = toValue;
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:fromValue];
[path addLineToPoint:toValue];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animation.path = path.CGPath;
animation.duration = 2.0 * (toValue.y - fromValue.y) / (y - baseY);
CAAnimationGroup *group = [CAAnimationGroup animation];
group.duration = animation.duration;
group.animations = #[animation];
[layer addAnimation:group forKey:animation.keyPath];
}
You can find the full code for my test program in this gist. Just create a new Single View Application project and replace the contents of ViewController.m with the contents of the gist.

Resources