Skip to content

Instantly share code, notes, and snippets.

@Kyriakos-Georgiopoulos
Last active September 12, 2025 02:08
Show Gist options
  • Save Kyriakos-Georgiopoulos/470c3486002a0165a95aa8b453c5d62f to your computer and use it in GitHub Desktop.
Save Kyriakos-Georgiopoulos/470c3486002a0165a95aa8b453c5d62f to your computer and use it in GitHub Desktop.
Jetpack Compose Animation Lesson — Time Remapping with a Single Fraction

Jetpack Compose Animation Lesson — Time Remapping with a Single Fraction

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.


What is Time Remapping?

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.


How It Works

  1. Animate one master fraction with Animatable(0f..1f)
  2. Define helper functions:
    • lead(f) → push time forward
    • lag(f) → pull time backward
    • pause(f) → flatten a segment to hold still
  3. Apply an easing to the remapped fraction
  4. 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.


Code Example

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)

Why This Matters

  • 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment