In Jetpack Compose, many animations are driven by a single fraction t ∈ [0,1]
.
For example, you might use lerp(start, end, t)
to animate position, size, or color in sync.
But what if you want different properties to feel slightly out of sync?
This is where time remapping comes in.
Instead of using the same t
directly for all properties, you remap time per property.
This means each property computes its own local fraction before applying easing and interpolation.
- Lead: property runs slightly ahead of the base timeline
- Lag: property stays slightly behind
- Pause: property holds still for a segment of the timeline (like a keyframe freeze)
With this technique, you can make parts of your animation feel like they anticipate, follow, or pause without needing multiple timelines.
- Animate one master fraction with
Animatable(0f..1f)
- Define helper functions:
lead(f)
→ push time forwardlag(f)
→ pull time backwardpause(f)
→ flatten a segment to hold still
- Apply an easing to the remapped fraction
- Use
lerp
to calculate the property value
base t ──► remap per property (lead/lag/pause) ──► easing.transform(...) ──► lerp(...)
This creates a cinematic feel: properties don’t move in strict lockstep, yet everything is still driven by a single timeline.
val t = remember { Animatable(0f) }
// Loop master timeline
LaunchedEffect(Unit) {
while (true) {
t.animateTo(1f, tween(1600, easing = LinearEasing))
t.animateTo(0f, tween(1600, easing = LinearEasing))
}
}
val base = t.value
// Time remapping helpers
fun lead(f: Float, amount: Float = 0.1f) = (f * (1f + amount)).coerceIn(0f, 1f)
fun lag(f: Float, amount: Float = 0.15f) = (f * (1f - amount)).coerceIn(0f, 1f)
fun pause(f: Float, holdStart: Float = 0.35f, holdEnd: Float = 0.55f): Float =
when {
f <= holdStart -> f / holdStart * holdStart
f <= holdEnd -> holdStart
else -> {
val rem = 1f - holdEnd
holdStart + ((f - holdEnd) / rem) * (1f - holdStart)
}
}
// Remapped fractions
val posT = FastOutSlowInEasing.transform(lead(base))
val colorT = LinearOutSlowInEasing.transform(lag(pause(base)))
// Apply values
val position = lerp(0.dp, maxWidth - boxWidth, posT)
val boxColor = lerp(Color.Blue, Color.Cyan, colorT)
- Keeps your animation state simple → one fraction
- Adds variety and life by offsetting properties in time
- Easy to extend → just add more remapping strategies
This technique is a building block for more cinematic UI motion, where not everything happens at once, but still feels connected.