Animating a CALayer shadowpath - objective-c

I'm animating the shadow path for CALayer.
The frame resizes correctly, but the shadow does not scale.
Instead the shadow starts at the final size CGSize(20,20) and holds throughout the animation even though I set the shadowPath to an initial value
[CATransaction begin];
[CATransaction setAnimationDuration: 0];
[CATransaction setDisableActions: TRUE];
layer.frame = CGRectMake(0,0,10,10);
layer.shadowPath = [UIBezierPath bezierPathWithRect:layer.bounds].CGPath;
[CATransaction commit];
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:10] forKey:kCATransactionAnimationDuration];
layer.frame = CGRectMake(0,0,20,20);
layer.shadowPath = [UIBezierPath bezierPathWithRect:tile.bounds].CGPath;
[CATransaction commit];

At first, small square with drop shadow.
When button pushed, square and shadow grow bigger together.
The main code is below:
[CATransaction begin];
[CATransaction setAnimationDuration:5.0];
CAMediaTimingFunction *timing = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[CATransaction setAnimationTimingFunction:timing];
layer.frame = CGRectMake(0,0,100,100);
[CATransaction commit];
CABasicAnimation *shadowAnimation = [CABasicAnimation animationWithKeyPath:#"shadowPath"];
shadowAnimation.duration = 5.0;
shadowAnimation.fromValue = (id)[UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 50, 50)].CGPath;
shadowAnimation.toValue = (id)[UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 100, 100)].CGPath;
[layer addAnimation:shadowAnimation forKey:#"shadow"];
You can download this project from GitHub and just run it.
https://github.com/weed/p120812_CALayerShadowTest
This question was very hard for me ! :)

Wanted to add another answer based on Weed's answer. I took weed's answer and tried to put everything in a CATransaction because i wanted to animate multiple layers and be sure animations happened together. Here you guys go if you ever need it. Also, i still don't understand why you have to use the fromValue and toValue in a CATransaction. Why can't you just do the same thing you do with other properties like frame.
[CATransaction begin];
[CATransaction setValue:[CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseOut]
forKey:kCATransactionAnimationTimingFunction];
for (CALayer *layer in self.layers){
CABasicAnimation *shadowAnimation =
[CABasicAnimation animationWithKeyPath:#"shadowPath"];
shadowAnimation.fromValue =
(id)[UIBezierPath bezierPathWithRect:layer.bounds].CGPath;
shadowAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
layer.frame = rectFinal;
shadowAnimation.toValue =
(id)[UIBezierPath bezierPathWithRect:layer.bounds].CGPath;
layer.shadowPath =
[UIBezierPath bezierPathWithRect:layer.bounds].CGPath;
[layer addAnimation:shadowAnimation forKey:nil];
}
[CATransaction commit];

Related

CAShapeLayer animation flash when I set duration very small

I want to realize a round progress, and the progress value can be set dynamically. The code is followed:
- (CGPoint)center {
return CGPointMake(self.view.frame.origin.x + self.view.frame.size.width / 2.0,
self.view.frame.origin.y + self.view.frame.size.height / 2.0);
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
CGMutablePathRef roundPath = CGPathCreateMutable();
CGPathAddArc(roundPath, NULL, self.center.x, self.center.y,
20,
2 * M_PI + M_PI_2,
M_PI_2,
YES);
CAShapeLayer *backgroundLayer = [CAShapeLayer layer];
backgroundLayer.frame = self.view.layer.bounds;
backgroundLayer.path = roundPath;
backgroundLayer.strokeColor = [[NSColor blueColor] CGColor];
backgroundLayer.fillColor = nil;
backgroundLayer.lineWidth = 10.0f;
backgroundLayer.lineJoin = kCALineJoinBevel;
[self.view.layer addSublayer:backgroundLayer];
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.frame = self.view.layer.bounds;
pathLayer.path = roundPath;
pathLayer.strokeColor = [[NSColor whiteColor] CGColor];
pathLayer.fillColor = nil;
pathLayer.lineWidth = 10.0f;
pathLayer.lineJoin = kCALineJoinBevel;
[self.view.layer addSublayer:pathLayer];
self.pathLayer = pathLayer;
[self start];
}
- (void)start {
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 0.01;
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
pathAnimation.toValue = [NSNumber numberWithFloat:self.progress+0.01];
[self.pathLayer setStrokeEnd:self.progress + 0.01];
[pathAnimation setDelegate:self];
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
self.progress += 0.01;
if (self.progress < 1.0) {
[self start];
}
}
I found when I set the duration be 0.1f or even bigger, it will work right.But if I set the duration be 0.01f, the animation will not start from the correct value, it will animate from a bigger value then decrease to the correct value. So the whole animation always flash, anybody got the same question or know why? Thanks very much!
originaluser2 is probably right about the specific cause, but this design is incorrect.
At 60fps, one frame is 0.0167s. You're asking to animate the change in less than a single frame. Each of those animations has its own media timing (kCAMediaTimingFunctionDefault), which means your creating complicated ramp up/ramp down velocities through your animation. And your timing is going to be erratic anyway because you're picking up a little error in every step (animationDidStop can take slightly different amounts of time to run depending on many factors). The whole point of animations is that you don't need to do this kind of stuff. That's why you have an animation engine.
Just animate to progress over the time you want it to take. Don't try to inject many extra animation steps. Injecting steps is what the animation engine does.
CALayer is designed to do most of this stuff for you anyway. You don't need to be creating explicit animations for this; just use implicit. Something like (untested, uncompiled):
- (void)setProgress: (CGFloat)progress {
double velocity = 1.0;
[CATransaction begin];
double delta = fabs(_progress - progress);
[CATransaction setAnimationDuration: delta * velocity];
[self.pathLayer setStrokeEnd: progress];
[CATransaction commit];
}
As Rob says, adding repeated animations isn't the best idea. His solution will probably work out nicely for you. However if you're still insistent on using repeated animations, here's the fix for your current code:
The problem is you're calling this line of code before you add your animation.
[self.pathLayer setStrokeEnd:self.progress + 0.01];
This will create an implicit animation on the layer, and therefore when you come to add your explicit animation – it will cause problems (the flashes you were observing).
The solution is to update the model layer within a CATransaction after you start the animation. You'll also need to set disableActions to YES in order to prevent an implicit animation from being generated.
For example:
- (void)start {
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 0.01;
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
pathAnimation.toValue = [NSNumber numberWithFloat:self.progress+0.01];
[pathAnimation setDelegate:self];
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.pathLayer setStrokeEnd:self.progress + 0.01];
[CATransaction commit];
}
Although, it's also worth noting you could just create your animation by just using a CATransaction, and the implicit animation for strokeEnd.
For example:
- (void)start {
[CATransaction begin];
[CATransaction setAnimationDuration:0.01];
[CATransaction setCompletionBlock:^{
self.progress += 0.01;
if (self.progress < 1.0) {
[self start];
}
}];
[self.pathLayer setStrokeEnd:self.progress + 0.01];
[CATransaction commit];
}
That way you don't have to create a new explicit animation on each iteration.

CABasicAnimation comleted immediately after CALayer "setFrame", why?

-(void)startAnimationWithTime:(CGFloat)animationTime
fromPoint:(CGPoint)fromPoint
toPoint:(CGPoint)toPoint
Completion:(SYBDanmakuAnimationCompletion)completion
{
[CATransaction begin];
[CATransaction setDisableActions:YES];
[CATransaction setCompletionBlock:^{
completion(YES);
}];
CABasicAnimation *mover = [CABasicAnimation animationWithKeyPath:#"position"];
[mover setDuration:animationTime];
[mover setFromValue:[NSValue valueWithCGPoint:fromPoint]];
[mover setToValue:[NSValue valueWithCGPoint:toPoint]];
[self addAnimation:mover forKey:#"move"];
[CATransaction commit];
}
This is the code of myLayer ,when I change the frame of its superLayer , this animation will complete immediately, who can tell me why? I want it to work correctly.

Piechart not rotating on tap in Coreplot- iOS

Please refer to the code for rotating the piechart when any sice is touched:
-(void)pieChart:(CPTPieChart *)plot sliceWasSelectedAtRecordIndex:(NSUInteger)index{
[self rotatePieChart:plot];
}
-(void)rotatePieChart:(CPTPieChart *)plot {
[CATransaction begin];
{
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform"];
CATransform3D transform = CATransform3DMakeRotation(-45, 0, 0, 1);
rotation.toValue = [NSValue valueWithCATransform3D:transform];
rotation.fillMode = kCAFillModeForwards;
rotation.duration = 1.0f;
rotation.cumulative=YES;
rotation.removedOnCompletion = NO;
[plot addAnimation:rotation forKey:#"rotation"];
}
[CATransaction commit];
}
ON first Tap the piechart rotates but on subsequent taps nothing happens.
Instead of rotating the whole plot layer, animate only the startAngle property. I suspect the hit-detection code is getting confused by the rotated coordinate system.

Slow down & accelerate animations with CoreAnimation

I am trying to apply a slow-motion effect to my application, pretty much like how you can slow down most graphical effects of Mac OS if you press Shift.
My application uses CoreAnimation, so I thought it should be no biggie: set speed to some slower value (like 0.1) and set it back to 1 once I'm done, and here I go.
It seems, unfortunately, that this is not the right way. The slowdown works great, however when I want to get back to normal speed, it resumes as if the speed was 1 the whole time. This basically means that if I held Shift for long enough, as soon as I release it, the animation instantly completes.
I found a Technical QA page explaining how to pause and resume an animation, but I can't seem to get it right if it's not about entirely pausing the animation. I'm definitely not very good at time warping.
What would be the right way to slow down then resume an animation with CoreAnimation?
Here's the useful code:
-(void)flagsChanged:(NSEvent *)theEvent
{
CALayer* layer = self.layer;
[CATransaction begin];
CATransaction.disableActions = YES;
layer.speed = (theEvent.modifierFlags & NSShiftKeyMask) ? 0.1 : 1;
[CATransaction commit];
}
Tricky problem...
I set up a test with a basic UIView.
I initiate an animation of its layer from it's current point to a target point.
In order to slow down the core animation, I actually had to construct and replace a new animation (since you can't modify the existing animation).
It's especially important to figure out how far along the current animation has already proceeded. This is done by accessing beginTime, currentTime, calculating the elapsed time and then figuring out how long should be the duration of the new animation.
- (void)initiate {
if(!initiated) {
[CATransaction begin];
[CATransaction disableActions];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut]];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"position"];
ba.duration = 10.0f;
ba.fromValue = [NSValue valueWithCGPoint:view.layer.position];
ba.toValue = [NSValue valueWithCGPoint:CGPointMake(384, 512)];
[view.layer addAnimation:ba forKey:#"animatePosition"];
[CATransaction commit];
initiated = YES;
}
}
- (void)slow {
[CATransaction begin];
CABasicAnimation *old = (CABasicAnimation *)[view.layer animationForKey:#"animatePosition"];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"position"];
CFTimeInterval animationBegin = old.beginTime;
CFTimeInterval currentTime = CACurrentMediaTime();
CFTimeInterval elapsed = currentTime - animationBegin;
ba.duration = [[old valueForKey:#"duration"] floatValue] - elapsed;
ba.duration = [[old valueForKey:#"duration"] floatValue];
ba.autoreverses = [[old valueForKey:#"autoreverses"] boolValue];
ba.repeatCount = [[old valueForKey:#"repeatCount"] floatValue];
ba.fromValue = [NSValue valueWithCGPoint:((CALayer *)[view.layer presentationLayer]).position];
ba.toValue = [old valueForKey:#"toValue"];
ba.speed = 0.1;
[view.layer addAnimation:ba forKey:#"animatePosition"];
[CATransaction commit];
}
- (void)normal {
[CATransaction begin];
CABasicAnimation *old = (CABasicAnimation *)[view.layer animationForKey:#"animatePosition"];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"position"];
CFTimeInterval animationBegin = old.beginTime;
CFTimeInterval currentTime = CACurrentMediaTime();
CFTimeInterval elapsed = currentTime - animationBegin;
ba.duration = [[old valueForKey:#"duration"] floatValue] - elapsed;
ba.autoreverses = [[old valueForKey:#"autoreverses"] boolValue];
ba.repeatCount = [[old valueForKey:#"repeatCount"] floatValue];
ba.fromValue = [NSValue valueWithCGPoint:((CALayer *)[view.layer presentationLayer]).position];
ba.toValue = [old valueForKey:#"toValue"];
ba.speed = 1;
[view.layer addAnimation:ba forKey:#"animatePosition"];
[CATransaction commit];
}
Note: The above code works only in 1 direction, not with animations that autoreverse...

combine multiple CAAnimation sequentially

I was reading about CATransactions and then thought this might help solve my problem.
This is what I wan't to do:
I have 3 animations in the same layer, which all have their own duration. I create the animations using CAKeyframeAnimation with a CGMutablePathRef.
say for example:
anim1 -> duration 5s
anim2 -> 3s
anim3 -> 10s
Now I want to serialize them sequentially. I tried to use CAAnimationGroup but the animations run concurrently.
I read about CATransaction, is it a possible solution? Can you give me a little example?
thanks for help !
If by serialization you mean starting each animation after the previous one has finished, use the beginTime property (defined in CAMediaTiming protocol). Note that its documentation is a little misleading. Here's an example:
anim2.beginTime = anim1.beginTime + anim1.duration;
anim3.beginTime = anim2.beginTime + anim2.duration;
If You are sure to do this thing with Layers then you may try like following
Using Completion Block in CATransactions
-(void)animateThreeAnimationsOnLayer:(CALayer*)layer animation:(CABasicAnimation*)firstOne animation:(CABasicAnimation*)secondOne animation:(CABasicAnimation*)thirdOne{
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[CATransaction begin];
[CATransaction setCompletionBlock:^{
//If any thing on completion of all animations
}];
[layer addAnimation:thirdOne forKey:#"thirdAnimation"];
[CATransaction commit];
}];
[layer addAnimation:secondOne forKey:#"secondAnimation"];
[CATransaction commit];
}];
[layer addAnimation:firstOne forKey:#"firstAnimation"];
[CATransaction commit];
}
Another way is by applying delay to begin animation.
-(void)animateThreeAnimation:(CALayer*)layer animation:(CABasicAnimation*)firstOne animation:(CABasicAnimation*)secondOne animation:(CABasicAnimation*)thirdOne{
firstOne.beginTime=0.0;
secondOne.beginTime=firstOne.duration;
thirdOne.beginTime=firstOne.duration+secondOne.duration;
[layer addAnimation:firstOne forKey:#"firstAnim"];
[layer addAnimation:secondOne forKey:#"secondAnim"];
[layer addAnimation:thirdOne forKey:#"thirdAnim"];
}
And if You are going to use UIVIew Animation
//if View is applicable in your requirement then you can look this one;
-(void)animateThreeAnimationOnView{
[UIView animateWithDuration:2.0 animations:^{
//first Animation
} completion:^(BOOL finished) {
[UIView animateWithDuration:2.0 animations:^{
//Second Animation
} completion:^(BOOL finished) {
[UIView animateWithDuration:2.0 animations:^{
//Third Animation
}];
}];
}];
}