How to do a curve/arc animation with CAAnimation?

RickiG picture RickiG · Oct 27, 2010 · Viewed 16.1k times · Source

I have an user interface where an item get deleted, I would like to mimic the "move to folder" effect in iOS mail. The effect where the little letter icon is "thrown" into the folder. Mine will get dumped in a bin instead.

I tried implementing it using a CAAnimation on the layer. As far as I can read in the documentations I should be able to set a byValue and a toValue and CAAnimation should interpolate the values. I am looking to do a little curve, so the item goes through a point a bit above and to the left of the items start position.

    CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"position"];
[animation setDuration:2.0f];
[animation setRemovedOnCompletion:NO];
[animation setFillMode:kCAFillModeForwards];    
[animation setTimingFunction:[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]];
[animation setFromValue:[NSValue valueWithCGPoint:fromPoint]];
[animation setByValue:[NSValue valueWithCGPoint:byPoint]];
[animation setToValue:[NSValue valueWithCGPoint:CGPointMake(512.0f, 800.0f)]];
[animation setRepeatCount:1.0];

I played around with this for some time, but it seems to me that Apple means linear interpolation. Adding the byValue does not calculate a nice arc or curve and animate the item through it.

How would I go about doing such an animation?

Thanks for any help given.

Answer

Can picture Can · Jul 11, 2013

Using UIBezierPath

(Don't forget to link and then import QuartzCore, if you're using iOS 6 or prior)

Example code

You could use an animation that will follow a path, conveniently enough, CAKeyframeAnimation supports a CGPath, which can be obtained from an UIBezierPath. Swift 3

func animate(view : UIView, fromPoint start : CGPoint, toPoint end: CGPoint)
{
    // The animation
    let animation = CAKeyframeAnimation(keyPath: "position")

    // Animation's path
    let path = UIBezierPath()

    // Move the "cursor" to the start
    path.move(to: start)

    // Calculate the control points
    let c1 = CGPoint(x: start.x + 64, y: start.y)
    let c2 = CGPoint(x: end.x,        y: end.y - 128)

    // Draw a curve towards the end, using control points
    path.addCurve(to: end, controlPoint1: c1, controlPoint2: c2)

    // Use this path as the animation's path (casted to CGPath)
    animation.path = path.cgPath;

    // The other animations properties
    animation.fillMode              = kCAFillModeForwards
    animation.isRemovedOnCompletion = false
    animation.duration              = 1.0
    animation.timingFunction        = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseIn)

    // Apply it
    view.layer.add(animation, forKey:"trash")
}

Understanding UIBezierPath

Bezier paths (or Bezier Curves, to be accurate) work exactly like the ones you'd find in photoshop, fireworks, sketch... They have two "control points", one for each vertex. For example, the animation I just made:

enter image description here

Works the bezier path like that. See the documentation on the specifics, but it's basically two points that "pull" the arc towards a certain direction.

Drawing a path

One cool feature about UIBezierPath, is that you can draw them on screen with CAShapeLayer, thus, helping you visualise the path that it will follow.

// Drawing the path
let *layer          = CAShapeLayer()
layer.path          = path.cgPath
layer.strokeColor   = UIColor.black.cgColor
layer.lineWidth     = 1.0
layer.fillColor     = nil

self.view.layer.addSublayer(layer)

Improving the original example

The idea of calculating your own bezier path, is that you can make the completely dynamic, thus, the animation can change the curve it's going to do, based on multiple factors, instead of just hard-coding as I did in the example, for instance, the control points could be calculated as follows:

// Calculate the control points
let factor : CGFloat = 0.5

let deltaX : CGFloat = end.x - start.x
let deltaY : CGFloat = end.y - start.y

let c1 = CGPoint(x: start.x + deltaX * factor, y: start.y)
let c2 = CGPoint(x: end.x                    , y: end.y - deltaY * factor)

This last bit of code makes it so that the points are like the previous figure, but in a variable amount, respect to the triangle that the points form, multiplied by a factor which would be the equivalent of a "tension" value.