iOS animate/morph shape from circle to square
Asked Answered
O

5

13

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!

Ohmage answered 16/7, 2014 at 18:40 Comment(2)
It looks like you're having the same issue as this question [#17430297, based on the animations—might be worth a lookRhodos
hmm.. It may be - I know the question and followed the suggestion from accepted answer but still I is jus a dead end for me. I have the same number of segments/control points which I think helps since my animation is in a little bit better shape then the one from there but sill have no clue how to improve it.Ohmage
W
12

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

Williswillison answered 6/12, 2014 at 23:29 Comment(1)
good discovery re the two points, made an objective-c version.Quirites
E
7

I know this is an old question but this might help someone.

I think its better to animate corner radius to get this effect.

    let v1 = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
    v1.backgroundColor = UIColor.green
    view.addSubview(v1)

animation:

    let animation = CABasicAnimation(keyPath: "cornerRadius")
    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
    animation.fillMode = kCAFillModeForwards
    animation.isRemovedOnCompletion = false
    animation.fromValue = v1.layer.cornerRadius
    animation.toValue = v1.bounds.width/2
    animation.duration = 3
    v1.layer.add(animation, forKey: "cornerRadius")
Elis answered 7/2, 2017 at 9:21 Comment(0)
Q
1

Based on @Danchoys answer, here it is for Objective-C.

info is a button of 60px and infoVC is the next ViewController being pushed:

    UIBezierPath * maskPath = [UIBezierPath new];
    [maskPath addArcWithCenter:info.center radius:15.0f startAngle:DEGREES_TO_RADIANS(180) endAngle:DEGREES_TO_RADIANS(270) clockwise:true];
    [maskPath addArcWithCenter:info.center radius:15.0f startAngle:DEGREES_TO_RADIANS(270) endAngle:DEGREES_TO_RADIANS(0) clockwise:true];
    [maskPath addArcWithCenter:info.center radius:15.0f startAngle:DEGREES_TO_RADIANS(0) endAngle:DEGREES_TO_RADIANS(90) clockwise:true];
    [maskPath addArcWithCenter:info.center radius:15.0f startAngle:DEGREES_TO_RADIANS(90) endAngle:DEGREES_TO_RADIANS(180) clockwise:true];
    [maskPath closePath];

    UIBezierPath * endPath = [UIBezierPath new];
    [endPath moveToPoint:CGPointMake(0,0)];
    [endPath addLineToPoint:endPath.currentPoint];
    [endPath addLineToPoint:CGPointMake(w, 0)];
    [endPath addLineToPoint:endPath.currentPoint];
    [endPath addLineToPoint:CGPointMake(w, h)];
    [endPath addLineToPoint:endPath.currentPoint];
    [endPath addLineToPoint:CGPointMake(0, h)];
    [endPath addLineToPoint:endPath.currentPoint];
    [endPath closePath];

    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.frame = info.bounds;
    maskLayer.path = maskPath.CGPath;
    _infoVC.view.layer.mask = maskLayer;

    CABasicAnimation * animation = [CABasicAnimation animationWithKeyPath:@"path"];
    animation.fromValue = (id)maskPath.CGPath;
    animation.toValue = (id)endPath.CGPath;
    animation.duration = 0.3f;
    [maskLayer addAnimation:animation forKey:nil];
    [maskLayer setPath:endPath.CGPath];
Quirites answered 17/2, 2017 at 16:15 Comment(0)
A
0

I can highly recommend the library CanvasPod, it also has a predefined "morph" animation and is easy to integrate. You can also check out examples on the website canvaspod.io.

Asymmetry answered 7/12, 2014 at 0:20 Comment(0)
B
0

This is handy for people making a recording button and want a native iOS like animation.

Instantiate this class within the UIViewController you would like to have the button. Add a UITapGestureRecogniser and call animateRecordingButton when tapped.

import UIKit

class RecordButtonView: UIView {

    enum RecordingState {
        case ready
        case recording
    }
            
    var currentState: RecordingState = .ready

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.isUserInteractionEnabled = true
        self.backgroundColor = .white
        // My button is 75 points in height and width so this
        // Will make a circle
        self.layer.cornerRadius = 35
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    func animateRecordingButton() {
       
        var newRadius: CGFloat
        var scale: CGFloat
        
        switch currentState {
        case .ready:
            // The square radius
            newRadius = 10
            // Lets scale it down to 70%
            scale = 0.7
            currentState = .recording
        case .recording:
            // Animate back to cirlce 
            newRadius = 35
            currentState = .ready
            // Animate back to full scale
            scale = 1
        }
                    
        UIView.animate(withDuration: 1) {
            self.layer.cornerRadius = newRadius
            self.transform = CGAffineTransform(scaleX: scale, y: scale)
        }
        
    }

}
Bludge answered 3/12, 2020 at 2:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.