iOS animate/morph shape from circle to square

fgeorgiew picture fgeorgiew · Jul 16, 2014 · Viewed 9.5k times · Source

I try to change a circle into a square and vice versa and I am almost there. However it doesn't animates as expected. I would like all corners of a square to be animated/morphed at the same time but what I get is the following:

enter image description here

I use CAShapeLayer and CABasicAnimation in order to animate shape property.

Here is how I create the circle path:

- (UIBezierPath *)circlePathWithCenter:(CGPoint)center radius:(CGFloat)radius
{    
    UIBezierPath *circlePath = [UIBezierPath bezierPath];
    [circlePath addArcWithCenter:center radius:radius startAngle:0 endAngle:M_PI/2 clockwise:YES];
    [circlePath addArcWithCenter:center radius:radius startAngle:M_PI/2 endAngle:M_PI clockwise:YES];
    [circlePath addArcWithCenter:center radius:radius startAngle:M_PI endAngle:3*M_PI/2 clockwise:YES];
    [circlePath addArcWithCenter:center radius:radius startAngle:3*M_PI/2 endAngle:M_PI clockwise:YES];
    [circlePath closePath];
    return circlePath;
}

Here is the square path:

- (UIBezierPath *)squarePathWithCenter:(CGPoint)center size:(CGFloat)size
{
    CGFloat startX = center.x-size/2;
    CGFloat startY = center.y-size/2;

    UIBezierPath *squarePath = [UIBezierPath bezierPath];
    [squarePath moveToPoint:CGPointMake(startX, startY)];
    [squarePath addLineToPoint:CGPointMake(startX+size, startY)];
    [squarePath addLineToPoint:CGPointMake(startX+size, startY+size)];
    [squarePath addLineToPoint:CGPointMake(startX, startY+size)];
    [squarePath closePath];
    return squarePath;
}

I apply the circle path to one of my View's layers, set the fill etc. It draws perfectly. Then in my gestureRecognizer selector I create and run the following animation:

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
    animation.duration = 1;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

 animation.fromValue = (__bridge id)(self.stateLayer.path);
        animation.toValue = (__bridge id)(self.stopPath.CGPath);
self.stateLayer.path = self.stopPath.CGPath;

[self.stateLayer addAnimation:animation forKey:@"animatePath"];

As you can notice in the circlePathWithCenter:radius: and squarePathWithCenter:size: I follow the suggestion from here (to have the same number of segments and control points): Smooth shape shift animation

The animation looks better then in the post from above but it is still not the one I want to achieve :(

I know that I can do it with simple CALayer and then set appropriate level of cornerRadius to make a circle out of square/rectangle and after that animate cornerRadius property to change it from circle to square but I'm still extremaly curious if it is even possible to do it with CAShapeLayer and path animation.

Thanks in advance for help!

Answer

Danchoys picture Danchoys · Dec 7, 2014

After playing with it for a little while in the playground I noticed that each arc adds two segments to the bezier path, not just one. Moreover, calling closePath adds two more. So to keep the number of segments consistent, I ended up with adding fake segments to my square path. The code is in Swift, but I think it doesn't matter.

func circlePathWithCenter(center: CGPoint, radius: CGFloat) -> UIBezierPath {
    let circlePath = UIBezierPath()
    circlePath.addArcWithCenter(center, radius: radius, startAngle: -CGFloat(M_PI), endAngle: -CGFloat(M_PI/2), clockwise: true)
    circlePath.addArcWithCenter(center, radius: radius, startAngle: -CGFloat(M_PI/2), endAngle: 0, clockwise: true)
    circlePath.addArcWithCenter(center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI/2), clockwise: true)
    circlePath.addArcWithCenter(center, radius: radius, startAngle: CGFloat(M_PI/2), endAngle: CGFloat(M_PI), clockwise: true)
    circlePath.closePath()
    return circlePath
}

func squarePathWithCenter(center: CGPoint, side: CGFloat) -> UIBezierPath {
    let squarePath = UIBezierPath()
    let startX = center.x - side / 2
    let startY = center.y - side / 2
    squarePath.moveToPoint(CGPoint(x: startX, y: startY))
    squarePath.addLineToPoint(squarePath.currentPoint)
    squarePath.addLineToPoint(CGPoint(x: startX + side, y: startY))
    squarePath.addLineToPoint(squarePath.currentPoint)
    squarePath.addLineToPoint(CGPoint(x: startX + side, y: startY + side))
    squarePath.addLineToPoint(squarePath.currentPoint)
    squarePath.addLineToPoint(CGPoint(x: startX, y: startY + side))
    squarePath.addLineToPoint(squarePath.currentPoint)
    squarePath.closePath()
    return squarePath
}

enter image description here