2

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?

Hamish
  • 78,605
  • 19
  • 187
  • 280
roledene JKS
  • 405
  • 2
  • 8
  • 12

2 Answers2

11

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

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • 1
    Hi, I have a problem with your code: I removed the autoreverse but when the animation did end, my view is going back to its original state instantly... Any idea? Thanks! – Paul Bénéteau Nov 18 '17 at 19:22
3

In order to get a smooth animation, you want to look at animating the cornerRadius property instead of messing about with bezier paths.

So the animation is going to go something like this:

  1. Animate corner radius from current value down to zero
  2. Animate width out to the screen width
  3. Reverse width animation
  4. Reverse corner radius animation

So, let's get started by defining some properties that we're going to be working with:

class ViewController: UIViewController {

    let animLayer = CALayer() // the layer that is going to be animated
    let cornerRadiusAnim = CABasicAnimation(keyPath: "cornerRadius") // the corner radius reducing animation
    let cornerRadiusUndoAnim = CABasicAnimation(keyPath: "cornerRadius") // the corner radius increasing animation
    let widthAnim = CABasicAnimation(keyPath: "bounds.size.width") // the 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)

    ...

Here we define the layer we're going to be working with, the animations, the duration of one of the animation 'segments' and the size of the CALayer.

Next, let's setup our animations in the viewDidLoad

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: kCAMediaTimingFunctionEaseIn) // timing function to make it look nice


    // inverse of the cornerRadiusAnim
    cornerRadiusUndoAnim.duration = animDuration
    cornerRadiusUndoAnim.fromValue = 0;
    cornerRadiusUndoAnim.toValue = animLayer.cornerRadius
    cornerRadiusUndoAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) // timing function to make it look nice

    // increases the width, and autoreverses on completion
    widthAnim.duration = animDuration
    widthAnim.fromValue = animLayer.frame.size.width
    widthAnim.toValue = rect.size.width
    widthAnim.autoreverses = true
    widthAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) // timing function to make it look nice
    widthAnim.delegate = self // so that we get notified when the width animation finishes

}

Nothing too difficult here, we just define our layer & animation properties. I also added some timing functions in order to make the animation look nice and smooth, instead of linear.

Next, let's start our animation. I'm going to be doing this in the touchesBegan function, but you can put this anywhere.

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    widthAnim.beginTime = CACurrentMediaTime()+animDuration // starts after the corner radius anim has finished

    animLayer.addAnimation(widthAnim, forKey: "widthAnim")
    animLayer.addAnimation(cornerRadiusAnim, forKey: "cornerRadius")

    CATransaction.begin()
    CATransaction.setDisableActions(true) // disables implicit animations
    animLayer.cornerRadius = 0
    CATransaction.commit()
}

Here we add our width and cornerRadius animations, assigning a delayed start to the width animation.

What's with the CATransation you ask? Well once the cornerRadius animation ends, Core Animation will snap the layer back to it's presentation layer. We don't want that, so we're going to set the value directly, while also making sure that Core Animation doesn't add an implicit animation when we do so. Using a CATransaction avoids this as it's considered bad practice to use removedOnCompletion = false & fillMode = kCAFillModeForwards.

Finally, we want to undo the corner radius animation, once the width animation has reversed. We can do this as we assigned a delegate to the width animation earlier, so we can override the animationDidStop function.

override func animationDidStop(anim: CAAnimation, finished flag: Bool) {

    animLayer.addAnimation(cornerRadiusUndoAnim, forKey: "cornerRadiusUndo")

    CATransaction.begin()
    CATransaction.setDisableActions(true)
    animLayer.cornerRadius = layerSize*0.5
    CATransaction.commit()
}

Again, we use a CATransaction to set the cornerRadius back to its original value. And that's it!


Final Result

enter image description here


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

Hamish
  • 78,605
  • 19
  • 187
  • 280