What are the standard timing curves, and how do speed, timeOffset and beginTime change a layer’s local time (the pause trick)?
Predict first
What are the control points of the named curves, and how does a layer convert parent time to local time?
import UIKit
for n: CAMediaTimingFunctionName in [.easeInEaseOut, .default, .linear] {
var p1: [Float] = [0, 0], p2: [Float] = [0, 0]
let f = CAMediaTimingFunction(name: n)
f.getControlPoint(at: 1, values: &p1); f.getControlPoint(at: 2, values: &p2)
print(p1, p2)
}
let a = CALayer(); a.speed = 2; a.timeOffset = 1
print(a.convertTime(3, from: nil), a.convertTime(0, from: nil))
let p = CALayer(); p.speed = 0; p.timeOffset = 0.5
print(p.convertTime(10, from: nil), p.convertTime(99, from: nil))
let b = CALayer(); b.beginTime = 5
print(b.convertTime(8, from: nil))
- [0.42, 0.0] [0.58, 1.0]; [0.25, 0.1] [0.25, 1.0]; [0.0, 0.0] [1.0, 1.0]; then 7.0 1.0; then 0.5 0.5; then 3.0
- [0.25, 0.1] [0.25, 1.0]; [0.42, 0.0] [0.58, 1.0]; [0.0, 0.0] [1.0, 1.0]; then 7.0 1.0; then 10.0 99.0; then 3.0
- [0.42, 0.0] [0.58, 1.0]; [0.25, 0.1] [0.25, 1.0]; [0.0, 0.0] [1.0, 1.0]; then 5.0 0.0; then 0.5 0.5; then 8.0
Mental model. A layer has its own clock: speed is how fast it ticks, timeOffset is where the hands start, and speed 0 stops the hands.
Easing is a cubic Bézier through (0,0), two control points and (1,1). The named CAMediaTimingFunctions read back as: linear (0,0)–(1,1); easeIn (0.42, 0) and (1, 1); easeOut (0, 0) and (0.58, 1); easeInEaseOut (0.42, 0) and (0.58, 1); and the default (0.25, 0.1) and (0.25, 1), a slightly quicker start than ease-in-out. UIKit’s curve options map to these, and an animation with no timing function is linear. Custom curves use CAMediaTimingFunction(controlPoints: ); a spring ignores them.
Separately, every layer and animation conforms to CAMediaTiming, which defines local time from its parent’s: local = (parent − beginTime) × speed + timeOffset. With speed 2 and timeOffset 1, parent time 3 is local time 7 and parent time 0 is 1; with beginTime 5, parent time 8 is local 3 (all read back with convertTime(_:from:) below). This gives the classic pause: set speed = 0 and timeOffset to the frozen local time, and every query of local time returns that same value no matter how the parent clock advances (0.5 for both 10 and 99 in the check). Resume by setting speed back to 1 and adjusting beginTime so time continues from where it froze. Prefer UIViewPropertyAnimator (pause and fractionComplete) in new code; the layer trick is for layers and for understanding what the animator does.
Local time is (parent minus beginTime) times speed plus timeOffset, and speed 0 freezes it.
Common traps
- Pausing by removing the animation. The layer snaps to the model value; freeze local time instead.
- Expecting
.defaultto equal ease-in-out. The default curve has different control points.
They might follow up with
- How do you scrub an animation by hand? Set
speed = 0and drivetimeOffset. - What does
beginTimedo? It delays the start in the parent’s time.
Related questions
- 9.4.1 How do implicit and explicit animations differ in Core Animation, and how does CATransaction fit in?
- 9.10.1 What does UIView.animate add to the layer, why is the model value already final, and what do…
- 9.10.2 How do spring parameters work: mass, stiffness, damping, duration and bounce, and why must a…
← How do spring parameters work: mass, stiffness, damping, duration and… How do you drive an animation per frame with CADisplayLink, and why… →