Kompoz0.1.1 Türkçe
Design

Animation

Kompoz has Compose's animation APIs: values that animate to a target, content that animates in and out, transitions of several values together, and low-level Animatable for gestures and coroutines.

Choosing an API

You want toUse
Animate one value when its target changesanimateFloatAsState, animateColorAsState
Show or hide content with an animationAnimatedVisibility
Switch between pieces of contentAnimatedContent, Crossfade
Animate a size changeModifier().animateContentSize()
Animate several values from one stateupdateTransition
Animate forever (pulses, spinners)rememberInfiniteTransition()
Drive an animation from a coroutine or a gestureAnimatable
Move an element from one screen to anotherSharedTransitionLayout and sharedElement

Animating a value

animate*AsState returns a value that animates to its target whenever the target changes:

void LikeButton() {
auto liked = remember([] { return mutableStateOf(false); });
float scale = animateFloatAsState(liked.get() ? 1.2f : 1.f, spring(Spring::DampingRatioMediumBouncy));
Color tint = animateColorAsState(liked.get() ? Color::hex(0xE53935) : colorScheme().onSurfaceVariant, 300);

IconButton([liked] { liked.set(!liked.get()); }, [&] {
Icon(liked.get() ? Icons::Filled::Favorite : Icons::Filled::FavoriteBorder, "Like", Modifier().scale(scale), tint);
});
}

Only the scope that reads the value recomposes on each frame of the animation, not its parents. For something that changes every frame, read the value while drawing instead, so nothing recomposes (see Phases).

The second argument is an animation spec, or a duration in milliseconds for a tween.

A color animates as Compose's does (Color.VectorConverter): in Oklab, its lightness and hue along, so black to white is a mid gray to the eye halfway (0x636363), and a spring that overshoots goes past its target there.

Animation specs

SpecBehavior
spring(dampingRatio, stiffness)Physics-based. The default; it keeps its velocity when the target changes mid-way. Spring::DampingRatioMediumBouncy, Spring::StiffnessLow and the other constants are Compose's.
tween(durationMillis, delayMillis, easing)A fixed duration along an easing curve: FastOutSlowInEasing (the default), LinearOutSlowInEasing, FastOutLinearInEasing, LinearEasing.
keyframes(durationMillis, {{atMillis, value}, …})Values at given times.
repeatable(iterations, spec, mode), infiniteRepeatable(spec, mode)A spec repeated, restarting or reversing (RepeatMode::Reverse).
snap(delayMillis)Jumps to the target.

Material components use motionScheme()'s springs, so they move the way Material 3 (and M3 Expressive with MaterialExpressiveTheme) says.

AnimatedVisibility

AnimatedVisibility animates its content in when visible becomes true and out when it becomes false. Once hidden, the content leaves the composition.

void Details() {
auto open = remember([] { return mutableStateOf(false); });
Column([&] {
TextButton([open] { open.set(!open.get()); }, [&] { Text(open.get() ? "Less" : "More"); });
AnimatedVisibility(open.get(), Modifier(), fadeIn() + expandVertically(), fadeOut() + shrinkVertically(), [] {
Text("Here are the details.", Modifier().padding(16));
});
});
}

Enter transitions are fadeIn, slideInHorizontally, slideInVertically, scaleIn, expandIn, expandHorizontally and expandVertically; exit transitions are their fadeOut, slideOut…, scaleOut and shrink… counterparts. Combine them with +. EnterTransition::None and ExitTransition::None animate nothing, for when the children animate themselves with animateEnterExit from the content's AnimatedVisibilityScope.

AnimatedContent and Crossfade

AnimatedContent animates between contents when its target state changes: the old content leaves while the new one enters, and the size animates between them.

void Counter() {
auto count = remember([] { return mutableStateOf(0); });
Row(Modifier(), {.verticalAlignment = Alignment::CenterVertically}, [&] {
IconButton([count] { count.set(count.get() - 1); }, [] { Icon(Icons::Filled::Remove, "Less"); });
AnimatedContent<int>(count.get(), [](const int& n) {
Text(std::to_string(n), {.font = typography().headlineMedium});
}, Modifier(), [](const int& from, const int& to) {
// up when counting up, down when counting down
return togetherWith(slideInVertically(to > from ? 1 : -1), slideOutVertically(to > from ? -1 : 1));
});
IconButton([count] { count.set(count.get() + 1); }, [] { Icon(Icons::Filled::Add, "More"); });
});
}

The content lambda gets the state it shows, which for leaving content is the old one: draw from that argument, not from the state variable. togetherWith(enter, exit) pairs the two transitions; .withSizeTransform(SizeTransform{.clip = false}) changes how the size animates.

Crossfade is the simple case: it fades between contents.

enum class Page { Home, Settings };

void PageContent(Page page) {
Crossfade<Page>(page, [](const Page& p) {
if (p == Page::Home) Text("Home"); else Text("Settings");
});
}

animateContentSize

Modifier().animateContentSize() animates a node's size whenever its content makes it grow or shrink:

void Expandable(const std::string& text) {
auto expanded = remember([] { return mutableStateOf(false); });
Text(text, Modifier().clickable([expanded] { expanded.set(!expanded.get()); }).animateContentSize(),
{.maxLines = expanded.get() ? 0 : 2, .overflow = TextOverflow::Ellipsis});
}

Put it before size modifiers in the chain, so it sees the size change.

updateTransition

When several values animate together towards one state, a transition keeps them in step and tells you when they have all arrived:

enum class BoxState { Collapsed, Expanded };

void TransitionBox() {
auto state = remember([] { return mutableStateOf(BoxState::Collapsed); });
auto t = updateTransition(state.get());
float size = t.animateFloat([](BoxState s) { return s == BoxState::Expanded ? 200.f : 80.f; });
Color color = t.animateColor([](BoxState s) {
return s == BoxState::Expanded ? Color::hex(0x6750A4) : Color::hex(0xB58392);
});
Box(Modifier().size(size).background(color, 16).clickable([state] {
state.set(state.get() == BoxState::Expanded ? BoxState::Collapsed : BoxState::Expanded);
}));
}

t.currentState() catches up with t.targetState() once every value has arrived, and t.isRunning() says whether any is still moving.

Infinite animations

rememberInfiniteTransition() animates values for as long as they are in the composition:

void Pulse() {
auto pulse = rememberInfiniteTransition().animateFloatState(0.85f, 1.f, infiniteRepeatable(tween(800), RepeatMode::Reverse));
Box(Modifier().size(64)
.graphicsLayer([pulse](GraphicsLayerScope& l) { l.scaleX = l.scaleY = pulse.get(); })
.background(colorScheme().primary, CircleShape));
}

animateFloatState and animateColorState return a state to read where it is needed: read while drawing, as here, it is only drawn again each frame. animateFloat and animateColor return the value directly, recomposing the reader each frame.

Animatable

Animatable<T> is a value you animate from coroutines: animateTo suspends until the animation ends, snapTo jumps, stop stops it. A new animateTo takes over from a running one, keeping its velocity, which makes it the tool for animations driven by gestures:

void Shake(bool error) {
auto offset = remember([] { return Animatable<float>(0.f); });
LaunchedEffect(error, [offset, error]() -> Task {
if (!error) co_return;
for (float x : {-12.f, 12.f, -8.f, 8.f, 0.f}) co_await offset.animateTo(x, tween(50));
});
Text("Wrong password", Modifier().offset([offset] { return Offset{offset.value(), 0}; }),
{.color = colorScheme().error});
}
  • animateTo(target, spec, initialVelocity) starts from a fling's velocity, so a released drag springs on naturally.
  • animateDecay(velocity, splineBasedDecay()) slows down from a velocity until it stops, like a fling.
  • updateBounds(lower, upper) keeps it within limits.
  • value() is observable; read it during layout or drawing for smooth motion.

See Gestures for combining it with dragging.

Shared elements

A shared element moves and resizes from its place in one piece of content to its place in another, as in Compose:

void Gallery() {
auto details = remember([] { return mutableStateOf(false); });
SharedTransitionLayout(Modifier().fillMaxSize(), [&](const SharedTransitionScope& shared) {
AnimatedContent<bool>(details.get(), [&, details](const AnimatedContentScope& scope, const bool& open) {
Box(Modifier()
.size(open ? 280 : 72)
.sharedElement(shared.rememberSharedContentState("photo"), scope)
.background(colorScheme().tertiary, 16)
.clickable([details] { details.set(!details.get()); }));
});
});
}

Elements with the same key in the leaving and the entering content become one element that animates between them, drawn above everything else. sharedBounds is for elements whose contents differ: both are shown, cross-fading, in the moving bounds. With navigation, shared elements move between screens.

See also

  • Phases: animating without recomposing.
  • Gestures: animations that follow the finger.
  • Graphics: graphicsLayer for animated transforms.
Kompoz is licensed under the Apache License 2.0. Code samples on this site are part of it.