Animating CAShapeLayer size change
Asked Answered
G

4

18

I am drawing a circle, with an initial radius of 200

self.circle = [CAShapeLayer layer];
self.circle.fillColor = nil;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.lineWidth = 7;
self.circle.bounds = CGRectMake(0, 0, 2 * radius, 2 * radius);
self.circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.radius, self.radius)

Can anyone please tell me how to animate a change to a radius of 100?

Gilmagilman answered 14/11, 2012 at 1:35 Comment(3)
Doesn't it animate if you modify the bounds in an animation block?Otway
@Gilmagilman Does my answer not work?Nicolettenicoli
@Otway Unfortunately not. I may be doing it wrong, but the circle just redraws.Gilmagilman
G
23

This is how I ended up doing it:

UIBezierPath *newPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(newRadius, newRadius) radius:newRadius startAngle:(-M_PI/2) endAngle:(3*M_PI/2) clockwise:YES];
CGRect newBounds = CGRectMake(0, 0, 2 * newRadius, 2 * newRadius);

CABasicAnimation* pathAnim = [CABasicAnimation animationWithKeyPath: @"path"];
pathAnim.toValue = (id)newPath.CGPath;

CABasicAnimation* boundsAnim = [CABasicAnimation animationWithKeyPath: @"bounds"];
boundsAnim.toValue = [NSValue valueWithCGRect:newBounds];

CAAnimationGroup *anims = [CAAnimationGroup animation];
anims.animations = [NSArray arrayWithObjects:pathAnim, boundsAnim, nil];
anims.removedOnCompletion = NO;
anims.duration = 2.0f;
anims.fillMode  = kCAFillModeForwards;

[self.circle addAnimation:anims forKey:nil];
Gilmagilman answered 14/11, 2012 at 13:32 Comment(2)
You shouldn't use removedOnCompletion, nor fillMode. You should do: self.circle = newPath.CGPath; [self.circle addAnimation:...]. That way, the path will be correct when the animation ends, AND you're not leaking an animation object.Bevin
I've used this approach before and it sometimes creates memory leaks. You're making the animation happen, but not changing the underlying value of self.circle.path in a permanent way... you're just leaving an unremoved animation attached that makes the path look like it was permanently changed. See my other answer for an approach that works more cleanly (and follows Apple's documentation). I struggled with this for a long time, but I finally understand how it works now!Figment
F
13

This is, I believe, the simpler and preferred way to do it:

UIBezierPath *newPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(newRadius, newRadius) radius:newRadius startAngle:(-M_PI/2) endAngle:(3*M_PI/2) clockwise:YES];
CABasicAnimation* pathAnim = [CABasicAnimation animationWithKeyPath:@"path"];
pathAnim.fromValue = (id)self.circle.path;
pathAnim.toValue = (id)newPath.CGPath;
pathAnim.duration = 2.0f;
[self.circle addAnimation:pathAnim forKey:@"animateRadius"];

self.circle.path = newPath.CGPath;

I got this approach from the Apple Documentation here (See listing 3-2). The important things here are that you're setting the fromValue and also setting the final value for self.circle's path to newPath.CGPath right after you set up the animation. An animation does not change the underlying value of the model, so in the other approach, you're not actually making a permanent change to the shape layer's path variable.

I've used a solution like the chosen answer in the past (using removedOnCompletion and fillMode etc) but I found that on certain versions of iOS they caused memory leaks, and also sometimes that approach just doesn't work for some reason.

With this approach, you're creating the animation explicitly (both where it starts from and where it ends), and you're specifying the final permanent value for the path at the end (with that last line), so that there's no need to mess with not removing the animation when it finishes (which I believe is what leads to memory leaks if you do this animation a lot of times... the unremoved animations build up in memory).

Also, I removed the animation of the bounds because you don't need it to animate the circle. Even if the path is drawn outside the bounds of the layer, it'll still be fully shown (see the docs for CAShapeLayer about that). If you do need to animate the bounds for some other reason, you can use the same approach (making sure to specify the fromValue and the final value for bounds).

Figment answered 21/4, 2016 at 13:13 Comment(1)
This animation causes warping to occur when I tried it. Here are some crops of the animation in progress. (Grows from small to large). After it ends it is normal.Marthena
N
2

You can use a CAAnimationGroup to animate the bounds and path of your CAShapeLayer:

- (void)animateToRadius:(CGFloat)newRadius {
   CAShapeLayer *shapeLayer = ...;

   UIBezierPath *newBezierPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(newRadius, newRadius)];
   NSValue *newBounds = [NSValue valueWithCGRect:CGRectMake(0,0,2*newRadius,2*newRadius);

   CAAnimationGroup *group = [CAAnimationGroup animation];

   NSMutableArray *animations  = [@[] mutableCopy];
   NSDictionary *animationData = @{@"path":newBezierPath.CGPath, @"bounds":newBounds};
   for(NSString *keyPath in animationData) {
      CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath];
      animation.toValue   = animationData[keyPath];
      [animations addObject:animation];
   }

   group.animations = [animations copy];
   group.duration = 2;
   group.removedOnCompletion = NO;
   group.fillMode = kCAFillModeForwards;

   [shapeLayer addAnimation:group forKey:nil];
}
Nicolettenicoli answered 14/11, 2012 at 1:47 Comment(1)
Thanks Jacob, but unfortunately this doesn't work. The circle stays the same size and just moves position.Gilmagilman
M
2

Here is a direct swift conversion of Jonathan's code should anyone need it.

func scaleShape(shape : CAShapeLayer){

    let newRadius : CGFloat = 50;

    let newPath: UIBezierPath = UIBezierPath(arcCenter: CGPointMake(newRadius, newRadius), radius: newRadius, startAngle: (CGFloat(-M_PI) / 2.0), endAngle: (3 * CGFloat(M_PI) / 2), clockwise: true);


    let newBounds : CGRect = CGRect(x: 0, y: 0, width: 2 * newRadius, height: 2 * newRadius);
    let pathAnim : CABasicAnimation = CABasicAnimation(keyPath: "path");

    pathAnim.toValue = newPath.CGPath;

    let boundsAnim : CABasicAnimation = CABasicAnimation(keyPath: "bounds");
    boundsAnim.toValue = NSValue(CGRect: newBounds);

    let anims: CAAnimationGroup = CAAnimationGroup();
    anims.animations = [pathAnim, boundsAnim];
    anims.removedOnCompletion = false;
    anims.duration = 2.0;
    anims.fillMode = kCAFillModeForwards;

    shape.addAnimation(anims, forKey: nil);
}
Might answered 20/2, 2016 at 15:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.