CAAnimation not working as expected on CALayer - ios

I am trying to animate the the appearance and then the disappearance of a UIBezierPath in a CAShapeLayer.
How I would like to do it is that at start, the path would be invisible by setting strokeStart and strokeEnd to 0. Then, in my animate method I would do a CABasicAnimation with setting the strokeEnd to 1, and after this is done, another animation where I set the strokeStart to 1, so the path would disappear again. Here is what I've tried:
- (void)animate
{
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
CABasicAnimation *appearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
appearingAnimation.duration = _duration / 2.0;
appearingAnimation.fromValue = #0;
appearingAnimation.toValue = #1;
CABasicAnimation *disappearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
disappearingAnimation.beginTime = _duration / 2.0;
disappearingAnimation.duration = _duration / 2.0;
disappearingAnimation.fromValue = #0;
disappearingAnimation.toValue = #1;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.duration = _duration;
animationGroup.animations = #[appearingAnimation, disappearingAnimation];
animationGroup.repeatCount = MAXFLOAT;
[layer addAnimation:animationGroup forKey:#"test"];
}
But the effect I am seeing is that the appearingAnimation is working properly, but as soon as it is done, the path instantly disappears without any animation. After this (_duration/2.0 time later) the whole thing starts over, so it would appear that the disappearingAnimation has no effect.
I tried another approach, but that only worked if the path was on my view.layer. As soon as I added it to a sublayer and tried to animate that one, the second (disappearing) animation had a glitch in it. Here is my second approach:
- (void)animate
{
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
layer.strokeStart = 0;
layer.strokeEnd = 1;
[CATransaction begin];
[CATransaction setCompletionBlock:^{
layer.strokeStart = 1;
layer.strokeEnd = 1;
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[self animate];
}];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
animation.duration = _duration / 2.0;
animation.fromValue = [NSNumber numberWithDouble:0];
animation.toValue = [NSNumber numberWithDouble:1];
[layer addAnimation:animation forKey:#"animation1"];
[CATransaction commit];
}];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = _duration / 2.0;
animation.fromValue = [NSNumber numberWithDouble:0];
animation.toValue = [NSNumber numberWithDouble:1];
[layer addAnimation:animation forKey:#"animation"];
[CATransaction commit];
}
Any idea why the first solution doesn't work at all, or why the second is only working properly on the view.layer and not on a sublayer?

When animating a layer property, you will want to set the layer property to the end value before adding the animation, as the animation changes are only applied to the presentation, not the model layer. Once the animation is completed it will be removed, leaving the layer in the state it was before.
Both the first and second methods you have outlined worked for me once a layer had been created (missing from your snippet):
- (void)animate
{
CAShapeLayer *layer = [CAShapeLayer layer];
CGFloat _duration = 2.0;
CGFloat radius = self.view.frame.size.width/2.0;
CGFloat startAngle = -(M_PI_2);
CGFloat endAngle = (3*M_PI_2);
CGPoint center = CGPointMake(self.view.frame.size.width/2.0, self.view.frame.size.height/2.0);
layer.path = [[UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES] CGPath];
layer.fillColor = [[UIColor clearColor] CGColor];
layer.strokeColor = [[UIColor blackColor] CGColor];
[self.view.layer addSublayer:layer];
... {animation code}
}

I think it is the problem of the value of _duration / 2.0. It may be the problem of floating number calculation.
Try not to halve the total duration but to make the durations of appearingAnimation and disappearingAnimation as the base duration:
#define DURATION_BASE 1
// You can choose the DURATION_BASE value whaterver you like
- (void)animate
{
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
CABasicAnimation *appearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
appearingAnimation.duration = DURATION_BASE;
appearingAnimation.fromValue = #0;
appearingAnimation.toValue = #1;
CABasicAnimation *disappearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
disappearingAnimation.beginTime = appearingAnimation.duration;
disappearingAnimation.duration = DURATION_BASE;
disappearingAnimation.fromValue = #0;
disappearingAnimation.toValue = #1;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.duration = appearingAnimation.duration+disappearingAnimation.duration;
animationGroup.animations = #[appearingAnimation, disappearingAnimation];
animationGroup.repeatCount = MAXFLOAT;
[layer addAnimation:animationGroup forKey:#"test"];
}
When you want to change durations of appearingAnimation and disappearingAnimation, you just need modify the code like
appearingAnimation.duration = DURATION_BASE*1.2;
disappearingAnimation.duration = DURATION_BASE*1.2;

Related

CAShapeLayer Animation without having to update the path

When animating a CAShapeLayer, is it necessary to constantly keep updating the path of the layer, or is there a way to rely on CABasicAnimation alone?
In the example below, I set up four circles paths and draw them.
Then I want to animate them down to disappear. After the animation has completed however, they spring back to their original paths.
int radius = halfWidth-30; //the radius is the distance out from the centre
trackActive.path = [UIBezierPath bezierPathWithArcCenter:cicleCenter radius:radius startAngle:degreesToRadians(circleStart) endAngle:degreesToRadians(circleEnd) clockwise:true].CGPath;
circleActive.path = [UIBezierPath bezierPathWithArcCenter:cicleCenter radius:radius startAngle:degreesToRadians(circleStart) endAngle:degreesToRadians(circleEnd) clockwise:true].CGPath;
radius = halfWidth - 10;
trackNew.path = [UIBezierPath bezierPathWithArcCenter:cicleCenter radius:radius startAngle:degreesToRadians(circleStart) endAngle:degreesToRadians(circleEnd) clockwise:true].CGPath;
circleNew.path = [UIBezierPath bezierPathWithArcCenter:cicleCenter radius:radius startAngle:degreesToRadians(circleStart) endAngle:degreesToRadians(circleEnd) clockwise:true].CGPath;
NSArray * layers = #[trackActive, circleActive, trackNew, circleNew];
for (CAShapeLayer * layer in layers){
[CATransaction begin];
CABasicAnimation * animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 1.0f;
animation.removedOnCompletion = false;
animation.fromValue = #(1.0);
animation.toValue = #(0.0);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[layer addAnimation:animation forKey:#"circleAnimation"];
[CATransaction commit];
}
I understand that I can add this to set the new path:
[CATransaction setCompletionBlock:^{
//set the new path
layer.path = [UIBezierPath bezierPathWithArcCenter:cicleCenter radius:radius startAngle:degreesToRadians(circleStart) endAngle:degreesToRadians(arcEnd) clockwise:true].CGPath;
}];
But would prefer avoid keep having to update the path, and instead rely on the animation layer exclusively. Is this possible?
Something like: layer.path = animation.path or animation.updatesOriginalPath=true; could be wishful thinking, not sure.
You can simplify your code and omit the spring effect:
[CATransaction begin];
[CATransaction setAnimationDuration:1.0];
[CATransaction setAnimationTimingFunction: kCAMediaTimingFunctionEaseInEaseOut];
layer.strokeEnd = 0.0;
[CATransaction commit];
The transaction will create and add an implicit animation object for you.
You can set a flag in a an animation so it stays where it was in the end and starts where it should start like this:
[animation setRemovedOnCompletion:NO];
[animation setFillMode:kCAFillModeBoth];
check also this answer:
What exactly does removedOnCompletion = NO do?

CABasicAnimation + UIBezierPath

I'm trying to create a circular bar such the one that you can find around the recording button in the native camera on iOS when you are doing a timelapse.
A circle is created along the animation, and once it is completed is removed again "naturally".
I tried next code:
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.lapseBtnOutlet.center.x, self.lapseBtnOutlet.center.y) radius:28 startAngle:2*M_PI*0-M_PI_2 endAngle:2*M_PI*1-M_PI_2 clockwise:YES].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor whiteColor].CGColor;
circle.lineWidth = 2.0;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = self.lapseInterval;
animation.removedOnCompletion = NO;
animation.fromValue = #(0);
animation.toValue = #(1);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[circle addAnimation:animation forKey:#"drawCircleAnimation"];
But my problem now is that I don't know how to "remove" the line from start-to-end.
I tried with autoreverse property, but it removes the circle from end-to-start instead from start-to-end. Any ideas?
You need to animate the strokeStart from 0 to 1, and when the animation finishes, remove the shape layer.
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:#selector(animateProperty:) withObject:#"strokeEnd" afterDelay:1];
[self performSelector:#selector(animateProperty:) withObject:#"strokeStart" afterDelay:3];
}
-(void)animateProperty:(NSString *) prop {
if (! self.circle) {
self.circle = [CAShapeLayer layer];
self.circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.lapseBtnOutlet.center.x, self.lapseBtnOutlet.center.y) radius:28 startAngle:2*M_PI*0-M_PI_2 endAngle:2*M_PI*1-M_PI_2 clockwise:YES].CGPath;
self.circle.fillColor = [UIColor clearColor].CGColor;
self.circle.strokeColor = [UIColor whiteColor].CGColor;
self.circle.lineWidth = 2.0;
[self.view.layer addSublayer:self.circle];
}
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:prop];
animation.delegate = ([prop isEqualToString:#"strokeStart"])? self : nil;
animation.duration = 1;
animation.removedOnCompletion = NO;
animation.fromValue = #0;
animation.toValue = #1;
[self.circle addAnimation:animation forKey:prop];
}
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
[self.circle removeFromSuperlayer];
self.circle = nil;
}

CABasicAnimation is not triggered after creation of the layer

I have CALayer subclass. After creation of the layer I want to animate affineTransform of layer but without success. New value is set but the change is not animated. If I trigger the animation after 1 second everything works perfect.
RCLClockDialLayer *layer = [RCLClockDialLayer layer];
layer.frame = CGRectMake(center.x, center.y, 2, self.radius);
layer.center = center;
layer.radius = self.radius;
layer.width = width;
layer.backgroundColor = [UIColor blackColor].CGColor;
layer.color = [UIColor whiteColor];
layer.affineTransform = CGAffineTransformRotate(layer.affineTransform, RCLDegreesToRadians(270));
[self addSubLayer:layer];
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformRotate(transform, RCLDegreesToRadians(80));
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"affineTransform"];
animation.duration = .5;
animation.fromValue = [layer valueForKey:#"affineTransform"];
animation.toValue = [NSValue valueWithCGAffineTransform:transform];
animation.fillMode = kCAFillModeForwards;
animation.cumulative = YES;
animation.removedOnCompletion = NO;
layer.affineTransform = transform;
[layer addAnimation:animation forKey:#"affineTransform"];
This is my code. I have tried many different solutions but nothing works. Do someone know what is the reason for this?

Piechart animation completed event

CGFloat radius = 100;
CAShapeLayer *pieShape = [CAShapeLayer layer];
pieShape.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(100, 300, radius, radius)
cornerRadius:radius].CGPath;
pieShape.fillColor = [UIColor clearColor].CGColor;
pieShape.strokeColor = [UIColor orangeColor].CGColor;
pieShape.lineWidth = 100;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 15.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
pathAnimation.removedOnCompletion = NO;
pathAnimation.fillMode = kCAFillModeForwards;
[pieShape addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
Above code is working fine.
But my problem is how can know that the duration 15 second is completed.
You can tell the animation has completed using the superclass CAAnimation delegate method:
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
If the duration is specified as 15.0 seconds and the animationDidStop has been invoked with finished set to YES, the you know that the time has elapsed.

CABasicAnimation originates from top right rather than center

I have a circle that I animate growing outwards and getting bigger. Unfortunately despite me setting the anchorPoint it doesnt animate from the center outwards, it seems to lock position top left. What is going on?
See this image that depicts the circles off center
Code
- (void)setLayerProperties {
rippleLayer = [[CAShapeLayer alloc] init];
rippleLayer.fillColor = [UIColor clearColor].CGColor;
rippleLayer.strokeColor = _Color.CGColor;
//rippleLayer.contentsGravity = #"center";
rippleLayer.anchorPoint = CGPointMake(0.5,0.5);
rippleLayer.bounds = self.bounds;
rippleLayer.path = bezierPath.CGPath;
[self.layer addSublayer:rippleLayer];
}
// Now animate the circle outwards
rippleLayer.opacity = 1;
CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
[scale setFromValue:[NSNumber numberWithFloat:1.0f]];
[scale setToValue:[NSNumber numberWithFloat:2.0f]];
[scale setRepeatCount:1];
[scale setDuration:1.0f];
//[scale setRemovedOnCompletion:YES];
[scale setFillMode:kCAFillModeForwards];
[rippleLayer addAnimation:scale forKey:scale.keyPath];
To scale layer at it's center, you need to set layer's position.This may help you
I want to draw a Smooth animations in iOS using CAPropertyAnimations
define RADIANS(angle) ((angle) / 180.0 * M_PI)
CGPoint testCenter = CGPointMake(144.5, 230.0);
CAShapeLayer *aTestLayer = [CAShapeLayer layer];
aTestLayer.path = [UIBezierPath bezierPathWithArcCenter:**CGPointZero** radius:15.0 startAngle:RADIANS(0) endAngle:RADIANS(360) clockwise:YES].CGPath;
aTestLayer.fillColor = [UIColor clearColor].CGColor;
aTestLayer.strokeColor = [UIColor whiteColor].CGColor;
**aTestLayer.position = testCenter;**
aTestLayer.borderWidth = 2.0;
[self.view.layer addSublayer:aTestLayer];
CABasicAnimation *pulse = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
pulse.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
pulse.duration = 2;
pulse.repeatCount = HUGE_VALF;
pulse.autoreverses = YES;
pulse.fromValue = [NSNumber numberWithFloat:1.0];
pulse.toValue = [NSNumber numberWithFloat:2.0];
[aTestLayer addAnimation:pulse forKey:nil];

Resources