---
title: Animations
source: 'https://academia.sh/en/courses/compose/animations'
course: 'UI Development with Jetpack Compose'
language: en
updated: '2026-10-21T05:02:38+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Measures that an animation's target value goes down from the owner of the state"
    - 'Shows that the per-frame intermediate value comes from a clock outside the tree'
    - 'Explains why an animation carries two separate moves'
    - "Distinguishes the linear interpolation as the model's calculation"
---

# Animations

Measuring that an animation carries two moves: the target value goes down from the owner of the state, while the per-frame intermediate value comes from the frame clock outside the tree and reaches the node with an outside move.

Say a panel's width goes from 0 to 100 when a button is pressed. The state changes in one step: the width must be 100. On the screen, though, the change shows up in a series of frames, and in every frame the width differs a little from the previous one. What produces these intermediate values is not the owner of the state, because the owner knows only the target.

This lesson separates the two pieces of data an animation carries. One is the target, and it comes from an ancestor in the tree. The other is the per-frame value, and it comes from a clock outside the tree. The same animation reaches the same node from two different directions.

```kotlin
// Direction.kt — classifies which direction a value travels in the UI tree
class Tree(private val parentOf: Map<String, String>) {
    fun contains(node: String): Boolean = node in parentOf.keys || node in parentOf.values
    fun parent(node: String): String? = parentOf[node]
    fun ancestors(node: String): List<String> {
        val chain = mutableListOf<String>()
        var current = parentOf[node]
        while (current != null) {
            chain.add(current)
            current = parentOf[current]
        }
        return chain
    }
}

class Move(val label: String, val from: String, val to: String, tree: Tree) {
    val direction: String = when {
        !tree.contains(from) || !tree.contains(to) -> "outside"
        to in tree.ancestors(from) -> "up"
        tree.parent(to) == from -> "down"
        from in tree.ancestors(to) -> "deep"
        else -> error("unrelated: $from and $to")
    }

    fun describe() {
        println("$label: from=$from | to=$to -> $direction")
    }
}

fun countDirections(moves: List<Move>) {
    for (name in listOf("down", "up", "deep", "outside")) {
        println("$name: ${moves.count { it.direction == name }}")
    }
}
```

## Target and Intermediate Value

State-based animations take a target value and walk the current value toward the target. The target is a piece of state, and the owner of the state is a component function in the tree. The platform's frame clock, on the other hand, runs outside the tree: every time the screen refreshes the clock advances one frame, and the animation computes the value in that frame.

```kotlin
// Platform code: an animation toward a target
@Composable
fun ExpandingPanel(expanded: Boolean) {
    val width by animateDpAsState(
        targetValue = if (expanded) 100.dp else 0.dp,
        label = "panelWidth",
    )
    Box(Modifier.width(width))
}
```

`expanded` is the state given to `ExpandingPanel` from outside, and it determines the target. `animateDpAsState` produces an intermediate value heading to that target. `Box` is measured again with this value in every frame. When the target changes the animation starts again; the frame clock goes on.

The model captures this platform rule; the model is not the platform itself. The helper file does two things: it computes the intermediate values the frame clock produces linearly, and it places an animated panel under the screen. The linear interpolation is the model's calculation; the platform's easing curve and duration setting are not here. Two functions also supply the ends of the measurements: `targetOwner` reads the panel's parent from the tree, and `clockSource` returns the name of the frame clock. In this lesson one end of a move is found from the tree (`targetOwner`); the other end is a source the model names that is not in the tree (`clockSource` returns a fixed name).

```kotlin
// Frames.kt — the intermediate values the frame clock produces (the model's calculation)
fun frameValues(from: Int, to: Int, steps: Int): List<Int> =
    (0..steps).map { from + (to - from) * it / steps }

fun animatedTree(): Tree = Tree(mapOf("Screen" to "App", "Panel" to "Screen"))

// the target's owner: the panel's parent, read from the tree
fun targetOwner(tree: Tree): String = tree.parent("Panel") ?: error("panel is the root")

// the source that produces the intermediate values: the frame clock outside the tree
fun clockSource(): String = "FrameClock"
```

## The Target Goes Down

The first measurement follows the target. `Screen` knows whether the panel is open or closed and gives it the target width.

```kotlin
// Main.kt — the animation's target value
fun main() {
    val tree = animatedTree()
    println("target: ${frameValues(0, 100, 2).last()}")
    Move("target", targetOwner(tree), "Panel", tree).describe()
}
```

```text
target: 100
target: from=Screen | to=Panel -> down
```

The target went down from `Screen` to `Panel`. This is the same move as the state flow in earlier lessons: the owner is the ancestor, the consumer is the child. The presence of an animation does not change the target's direction.

**LC1.** The model carries the target as a single integer. The target being a color, a distance or an opacity, and the animation being written with a separate function for each type, are not in the model.

## The Intermediate Value Comes from Outside

The second measurement follows the per-frame values. In every frame the panel gets a new width between 0 and 100. The frame clock that produces these values is not a node in the tree.

```kotlin
// Main.kt — the intermediate values coming from the frame clock
fun main() {
    val tree = animatedTree()
    val frames = frameValues(0, 100, 2)
    println("frames: ${frames.joinToString(",")}")
    val moves = frames.map { Move("frame-$it", clockSource(), "Panel", tree) }
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
frames: 0,50,100
frame-0: from=FrameClock | to=Panel -> outside
frame-50: from=FrameClock | to=Panel -> outside
frame-100: from=FrameClock | to=Panel -> outside
down: 0
up: 0
deep: 0
outside: 3
```

All three of the three frames are outside. Because the clock is not in the tree, one end of the move is outside the tree; unlike the target value, the intermediate value does not come from any ancestor in the tree. The panel takes each frame not through an ancestor but directly from the clock.

**LC2.** The model produces three frames and divides the intervals equally. The platform's number and duration of frames, the curves that slow down and speed up, and the target changing while the animation runs were not measured in the model.

## Who Holds the Intermediate Value

The intermediate value being outside is a design consequence. The intermediate value changes in every frame; if its owner were an ancestor, every frame would cause everything under that ancestor to be recomputed. Instead the node reading the intermediate value follows the value directly from the clock, and only that node is redrawn. The target, on the other hand, changes rarely, and when it changes it is natural for it to descend through the tree. The difference in speed is the reason the two pieces of data come by different paths; this lesson does not measure speed, only the direction of the path.

## The Bounding Measurement

An animation carrying two moves is the lesson's main finding. There is also a third move: when the animation finishes, a notification goes to the screen. The measurement below shows the three together.

```kotlin
// Main.kt — the moves an animation carries
fun main() {
    val tree = animatedTree()
    val frames = frameValues(0, 100, 2)
    val moves = listOf(
        Move("target", targetOwner(tree), "Panel", tree),
        Move("frame-${frames[1]}", clockSource(), "Panel", tree),
        Move("finish", "Panel", targetOwner(tree), tree),
    )
    for (move in moves) {
        move.describe()
    }
    countDirections(moves)
}
```

```text
target: from=Screen | to=Panel -> down
frame-50: from=FrameClock | to=Panel -> outside
finish: from=Panel | to=Screen -> up
down: 1
up: 1
deep: 0
outside: 1
```

One down, one up, one outside. The target is a move inside the tree, the intermediate value is a move outside the tree, and the finish notification is again a move inside the tree but in the opposite direction. The lesson's direction is the move that is specific to the animation: the intermediate value. The target and the finish notification would be found in the same way on a screen without an animation. The intermediate value exists only in an animation, and it always comes from outside the tree. The lesson's direction: **outside**.

The visibility animation and the transition object have the same structure. The visibility animation wraps an item's entry into and exit from the tree with an animation; the transition object (`updateTransition`) derives several values from a single target state. In both, the target comes from the tree and the per-frame values from the clock.

**LC3.** The model builds the finish notification as a single move. In which frame the notification is triggered and what happens when the animation is interrupted were not measured in the model. The visibility animation and the transition object are explained in this lesson only with a source snippet and are not measured.

## Summary

- This lesson's direction: outside. The frame clock that produces the intermediate value is not in the tree, so the move the lesson measures comes from outside the tree.
- An animation carries two pieces of data: the target value and the per-frame intermediate value.
- The target comes from the owner of the state and goes down to the child.
- The frame clock produces the intermediate value; because the clock is not in the tree, the move is outside.
- The notification that goes to the screen when the animation finishes goes up.

## Next Step

An animation shows the screen to everyone with the same change. For someone using a screen reader, however, the screen is a separate semantics tree derived from the tree. The next lesson measures how the labels of the children merge into the ancestor in this semantics tree, and the direction of that.
