Search code examples
iosswiftcore-animationcashapelayercabasicanimation

Circle to rectangle transformation animation


I am very new to iOS and I need to do following animation:

enter image description here

Transformation of the circle to rectangle should be smooth, but in above animation it's not very smooth.

What I did is create a circle and a rectangle using following code in this tutorial:

  Circle : 
        class OvalLayer: CAShapeLayer {

        let animationDuration: CFTimeInterval = 0.3

        override init() {
            super.init()
            fillColor = Colors.red.CGColor
            path = ovalPathSmall.CGPath
        }

        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        var ovalPathSmall: UIBezierPath {
            return UIBezierPath(ovalInRect: CGRect(x: 50.0, y: 50.0, width: 0.0, height: 0.0))
        }

        var ovalPathLarge: UIBezierPath {
            return UIBezierPath(ovalInRect: CGRect(x: 2.5, y: 17.5, width: 95.0, height: 95.0))
        }

        var ovalPathSquishVertical: UIBezierPath {
            return UIBezierPath(ovalInRect: CGRect(x: 2.5, y: 20.0, width: 95.0, height: 90.0))
        }

        var ovalPathSquishHorizontal: UIBezierPath {
            return UIBezierPath(ovalInRect: CGRect(x: 5.0, y: 20.0, width: 90.0, height: 90.0))
        }

        func expand() {
            let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
            expandAnimation.fromValue = ovalPathLarge.CGPath// change ovalPathLarge to ovalPathSmail for animation
            expandAnimation.toValue = ovalPathLarge.CGPath
            expandAnimation.duration = animationDuration
            expandAnimation.fillMode = kCAFillModeForwards
            expandAnimation.removedOnCompletion = false
            addAnimation(expandAnimation, forKey: nil)
        }

    }

Rectangle : 

    class RectangleLayer: CAShapeLayer {


    override init() {
        super.init()
        fillColor = Colors.clear.CGColor
        lineWidth = 5.0
        path = rectanglePathFull.CGPath
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    var rectanglePathFull: UIBezierPath {
        let rectanglePath = UIBezierPath()
        rectanglePath.moveToPoint(CGPoint(x: 0.0, y: 100.0))
        rectanglePath.addLineToPoint(CGPoint(x: 0.0, y: -lineWidth))
        rectanglePath.addLineToPoint(CGPoint(x: 100.0, y: -lineWidth))
        rectanglePath.addLineToPoint(CGPoint(x: 100.0, y: 100.0))
        rectanglePath.addLineToPoint(CGPoint(x: -lineWidth / 2, y: 100.0))
        rectanglePath.closePath()

//        fillColor = Colors.red.CGColor
        return rectanglePath
    }

//    var topLeft: UIBezierPath {}

    func animateStrokeWithColor(color: UIColor, view : UIView) {
        strokeColor = color.CGColor

//        CATransaction.setDisableActions(true)
//        view.layer.bounds.size.height = view.layer.bounds.width + 50

        let strokeAnimation: CABasicAnimation = CABasicAnimation(keyPath: "bounds.size.width") //bounds.size.width
        strokeAnimation.fromValue = view.layer.bounds.width
        strokeAnimation.toValue = view.layer.bounds.size.width - 50
        strokeAnimation.duration = 0.4
        addAnimation(strokeAnimation, forKey: nil)
    }
}

my view : 

    protocol HolderViewDelegate:class {
    func animateLabel()
}

class HolderView: UIView {

    let ovalLayer = OvalLayer()
    let redRectangleLayer = RectangleLayer()

    var parentFrame :CGRect = CGRectZero
    weak var delegate:HolderViewDelegate?

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = Colors.clear
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    func addOval() {
        layer.addSublayer(ovalLayer)
        ovalLayer.expand()
//        NSTimer.scheduledTimerWithTimeInterval(0.3, target: self, selector: "wobbleOval",
//            userInfo: nil, repeats: false)
    }

    func expandRectangle(){
        NSTimer.scheduledTimerWithTimeInterval(0.45, target: self,
            selector: "drawRedAnimatedRectangle",
            userInfo: nil, repeats: false)
    }

    func drawRedAnimatedRectangle() {
        layer.addSublayer(redRectangleLayer)
        redRectangleLayer.animateStrokeWithColor(Colors.red,view: self)
    }

But I have no idea how to do my animation, please can anyone help me?


Solution

  • If you want both the scaling up and corner radius reduction to happen at the same time, you can simplify the code from my other answer significantly.

    You now no longer need to 'chain' the animations together, so you can add them both to a single CAAnimationGroup and run them concurrently.

    The properties we use will remain almost identical, except with the addition of a groupAnim property and deletion of the cornerRadiusUndoAnim.

    class ViewController2: UIViewController {
    
        let animLayer = CALayer() // the layer that is going to be animated
        let cornerRadiusAnim = CABasicAnimation(keyPath: "cornerRadius") // the corner radius reducing animation
        let widthAnim = CABasicAnimation(keyPath: "bounds.size.width") // the width animation
        let groupAnim = CAAnimationGroup() // the combination of the corner and width animation
        let animDuration = NSTimeInterval(1.0) // the duration of one 'segment' of the animation
        let layerSize = CGFloat(100) // the width & height of the layer (when it's a square)
    
        ...        
    

    We can now just add the setup for the CAAnimationGroup, adding both our corner radius animation and our scaling animation

    override func viewDidLoad() {
        super.viewDidLoad()
    
        let rect = view.frame
    
        animLayer.backgroundColor = UIColor.blueColor().CGColor // color of the layer, feel free to change
        animLayer.frame = CGRect(x: rect.width-layerSize*0.5, y: rect.height-layerSize*0.5, width: layerSize, height: layerSize)
        animLayer.cornerRadius = layerSize*0.5;
        animLayer.anchorPoint = CGPoint(x: 1, y: 1) // sets so that when the width is changed, it goes to the left
        view.layer.addSublayer(animLayer)
    
        // decreases the corner radius
        cornerRadiusAnim.duration = animDuration
        cornerRadiusAnim.fromValue = animLayer.cornerRadius
        cornerRadiusAnim.toValue = 0;
        cornerRadiusAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) // timing function to make it look nice
    
        // increases the width
        widthAnim.duration = animDuration
        widthAnim.fromValue = animLayer.frame.size.width
        widthAnim.toValue = rect.size.width
        widthAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) // timing function to make it look nice
    
        // adds both animations to a group animation
        groupAnim.animations = [cornerRadiusAnim, widthAnim]
        groupAnim.duration = animDuration;
        groupAnim.autoreverses = true; // auto-reverses the animation once completed
    
    }
    

    Finally, we can run the group animation when the view gets touched, and both animations will run concurrently together (and auto-reverse when done).

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        animLayer.addAnimation(groupAnim, forKey: "anims") // runs both animations concurrently
    }
    

    Result

    enter image description here


    Full project: https://github.com/hamishknight/Circle-to-Rect-Animation