Irbisa · Cheatsheet

Compose Animation & Gestures

animate*AsState, Transition, AnimatedContent, Animatable, pointerInput, drag, nestedScroll, Canvas

Middle Developer · 24 questions

  1. 01

    How do you animate a composable's size when its content grows, for example an expandable card?

    Add Modifier.animateContentSize() to the container: whenever its measured size changes, it animates from the old size to the new one instead of jumping.

    @Composablefun ExpandableCard(title: String, details: String) {  var expanded by remember { mutableStateOf(false) }  Card(onClick = { expanded = !expanded }) {    Column(      Modifier…
  2. 02

    How do you animate a composable appearing and disappearing, and how do you customize the enter and exit animation?

    Wrap it in AnimatedVisibility(visible = ...); it composes the content when visible is true, plays an EnterTransition on the way in and an ExitTransition on the way out, and removes the content from the composition when the exit ends.

    @Composablefun Details(details: String) {  var visible by remember { mutableStateOf(false) }  Column {    Button(onClick = { visible = !visible }) { Text("Toggle") }    AnimatedVisibility(…
  3. 03

    How do you make an animation repeat forever, such as a pulsing dot or a loading shimmer?

    Create a rememberInfiniteTransition() and animate values on it with animateFloat or animateColor and an infiniteRepeatable spec; it starts when it enters the composition and stops when it leaves.

    @Composablefun PulsingDot(modifier: Modifier = Modifier) {  val transition = rememberInfiniteTransition(label = "pulse")  val scale by transition.animateFloat(    initialValue = 0.8f,    targetValue = 1.2f,…
  4. 04

    The counter below animates, but the leaving and the arriving text both show the new number. What is wrong?

    @Composablefun Total(count: Int) {  AnimatedContent(    targetState = count,    transitionSpec = {      slideInVertically { it } togetherWith slideOutVertically { -it }    },    label = "total",  ) {    Text("Total: $count")  }}

    The content lambda ignores its own parameter and reads the captured count, so the leaving copy recomposes with the new value too.

  5. 05

    When do you use clickable, combinedClickable or pointerInput { detectTapGestures } to handle taps?

    Use clickable for a plain tap and combinedClickable when you also need a long press or double tap; drop to detectTapGestures only when the tap is not a click, for example when the tap position on a chart matters.

    // a post: tap opens, double tap likes, long press shows the menuBox(  Modifier    .clip(MaterialTheme.shapes.medium)   // clip first, so the ripple is rounded    .combinedClickable(      onClick = onOpen,…
  6. 06

    How do you draw custom graphics in Compose, and what is the difference between Canvas, drawBehind and drawWithContent?

    All three give you a DrawScope with drawCircle, drawLine, drawArc, drawPath and friends: Canvas is a standalone empty element, drawBehind paints behind an existing composable, and drawWithContent lets you paint over its children.

    @Composablefun ProgressRing(progress: () -> Float, modifier: Modifier = Modifier) {  val track = MaterialTheme.colorScheme.surfaceVariant  val fill = MaterialTheme.colorScheme.primary  Canvas(modifier.size(64.dp)) {   // progress() is read inside the draw lambda: redraw only    val stroke = Stroke(width = 6.dp.toPx(), cap = StrokeCap.Round)…
  7. 07

    What is the difference between animate*AsState, updateTransition and Animatable, and when do you pick each?

    animate*AsState animates one value towards a target; updateTransition drives several values from one state so they stay in sync; Animatable is an imperative, coroutine-driven value for when a gesture or an event must control the animation.

  8. 08

    Versions A and B animate the same two Animatables. How long does each take, and in what order do the values move?

    val alpha = remember { Animatable(0f) }val offsetY = remember { Animatable(40f) } // version ALaunchedEffect(Unit) {  alpha.animateTo(1f, tween(300))  offsetY.animateTo(0f, tween(300))} // version BLaunchedEffect(Unit) {  launch { alpha.animateTo(1f, tween(300)) }  launch { offsetY.animateTo(0f, tween(300)) }}

    A runs them one after another (alpha first, then offsetY) and takes 600 ms, because animateTo is a suspend function that returns only when its animation ends.

  9. 09

    Find the bug: each time progress changes, this bar drops to empty and refills from zero instead of moving on from its current width.

    @Composablefun ProgressBar(progress: Float, modifier: Modifier = Modifier) {  val animated = Animatable(0f)  LaunchedEffect(progress) {    animated.animateTo(progress, animationSpec = tween(500))  }  Box(    modifier      .fillMaxWidth()      .height(8.dp)      .drawBehind {        drawRect(Color(0xFF3D7BFF), size = size.copy(width = size.width * animated.value))      }  )}

    Animatable(0f) is created in the composable body without remember, so every recomposition builds a fresh instance at 0; the restarted LaunchedEffect animates that new one from zero and the old value is lost.

  10. 10

    When do you choose spring, tween or keyframes as an animation spec, and what is visibilityThreshold for?

    Use spring for motion that can be interrupted or follows a gesture, because it carries the current velocity into a new target; tween when a designer specified an exact duration and easing; keyframes for multi-step motion such as an overshoot.

    // exact curve from the design specval alpha by animateFloatAsState(  targetValue = if (visible) 1f else 0f,  animationSpec = tween(300, easing = CubicBezierEasing(0.2f, 0f, 0f, 1f)),  label = "alpha",)…
  11. 11

    How do you animate items being added, removed and reordered in a LazyColumn?

    Add Modifier.animateItem() to each item's root and give the items stable keys; insertions then fade in, removals fade out and reordered items slide to their new positions.

    @Composablefun TaskList(tasks: List<Task>, onToggle: (String) -> Unit) {  LazyColumn {    items(tasks, key = { it.id }) { task ->      // stable, unique key      TaskRow(        task = task,…
  12. 12

    How do shared element transitions work in Compose, and how do you wire them up with Navigation Compose?

    Wrap both screens in one SharedTransitionLayout, mark the matching elements with Modifier.sharedElement using the same key, and give each the AnimatedVisibilityScope of the transition that swaps the screens; in a NavHost that is the receiver of the composable { } lambda.

    @Serializable object Covers@Serializable data class Detail(val id: String) @OptIn(ExperimentalSharedTransitionApi::class)@Composablefun App(nav: NavHostController) {…
  13. 13

    How does Compose treat the system "Remove animations" setting, and what do you still have to handle yourself?

    Compose scales its animations by the system animator duration scale, so at 0 an animation started from the composition jumps straight to its end; you still have to handle looping motion and animations driven from outside the composition.

  14. 14

    What does Modifier.graphicsLayer do, and why is it the preferred way to animate alpha, scale and rotation?

    graphicsLayer draws the composable into its own render layer with transform properties (alpha, scaleX/Y, rotationZ, translationX/Y, shadowElevation, clip); changing them redraws the layer without recomposing or re-measuring the content.

    @Composablefun FlipCard(flipped: Boolean, front: @Composable () -> Unit, back: @Composable () -> Unit) {  val rotation by animateFloatAsState(if (flipped) 180f else 0f, label = "flip")  val showBack by remember { derivedStateOf { rotation > 90f } }  Box(    Modifier.graphicsLayer {              // rotation is read here, in the draw phase…
  15. 15

    How do you write a custom gesture with Modifier.pointerInput, and what do its keys control?

    pointerInput(key) { } runs a suspending block where you await pointer events, either through helpers like detectDragGestures or by hand with awaitEachGesture and awaitFirstDown; the block restarts only when a key changes.

    @Composablefun VolumeStrip(onChange: (Float) -> Unit, modifier: Modifier = Modifier) {  val change by rememberUpdatedState(onChange)   // always the latest callback  Box(    modifier      .size(48.dp, 200.dp)…
  16. 16

    Find the bug: while you drag this chip, it runs ahead of the finger, and the gap grows the farther you drag.

    @Composablefun DraggableChip(label: String) {  var offsetX by remember { mutableFloatStateOf(0f) }  Text(    text = label,    modifier = Modifier      .offset(x = offsetX.dp)      .pointerInput(Unit) {        detectHorizontalDragGestures { change, dragAmount ->          change.consume()          offsetX += dragAmount        }      }  )}

    Drag deltas arrive in pixels, but offsetX.dp treats the pixel sum as dp, so the chip moves density times farther than the finger (2.75× on a typical 440 dpi phone); only on a density-1.0 screen does it look right.

  17. 17

    A drag handle built with detectDragGestures inside a LazyColumn row blocks the list from scrolling. How does Compose decide which gesture wins?

    The first node to consume() a pointer change owns the gesture.

  18. 18

    How do you implement pinch-to-zoom and pan on an image in Compose?

    Use Modifier.transformable with rememberTransformableState, or detectTransformGestures inside pointerInput; both report zoom, pan and rotation deltas per event, which you accumulate in state and apply through graphicsLayer.

    @Composablefun ZoomableImage(painter: Painter, modifier: Modifier = Modifier) {  var scale by remember { mutableFloatStateOf(1f) }  var offset by remember { mutableStateOf(Offset.Zero) }  val state = rememberTransformableState { zoomChange, panChange, _ ->    scale = (scale * zoomChange).coerceIn(1f, 5f)…
  19. 19

    How do you build a swipe-to-dismiss row, or any draggable that snaps to fixed positions?

    For the standard case use Material 3's SwipeToDismissBox; for custom snap points use AnchoredDraggableState with DraggableAnchors and Modifier.anchoredDraggable, and move the content by its offset yourself.

    enum class Swipe { Settled, Dismissed }@Composablefun SwipeToDeleteRow(item: Item, onDelete: () -> Unit) {  val state = remember { AnchoredDraggableState(initialValue = Swipe.Settled) }  LaunchedEffect(state.settledValue) {            // act when the animation lands    if (state.settledValue == Swipe.Dismissed) onDelete()…
  20. 20

    Your custom drawing allocates a Path and a Brush on every frame inside drawBehind. What does drawWithCache change?

    drawWithCache splits drawing in two: the outer block runs only when the size or a state it reads changes, and the onDrawBehind or onDrawWithContent block it returns runs every frame, so expensive objects are built once and reused.

    @Composablefun Gauge(progress: Float, modifier: Modifier = Modifier) {  val sweep by animateFloatAsState(progress * 270f, label = "sweep")  Box(    modifier      .size(160.dp)…
  21. 21

    How do you make a dragged element keep moving with the finger's velocity after release, and what does Animatable need during and after the drag?

    During the drag, snapTo the finger position so nothing animates; on release, hand the measured velocity to animateDecay to coast, or to animateTo(target, initialVelocity = v) to spring to a resting point without losing the throw.

    @Composablefun FlingCard(modifier: Modifier = Modifier) {  val offsetX = remember { Animatable(0f) }  val scope = rememberCoroutineScope()  val decay = rememberSplineBasedDecay<Float>()…
  22. 22

    How does nested scrolling work in Compose, and how do you build a collapsing header with a NestedScrollConnection?

    Each scroll delta first goes to the parents' onPreScroll, outermost first, then to the scrolling child, and what is left travels back up through onPostScroll; a collapsing header shrinks in pre-scroll and expands only in post-scroll, once the list has reached its top.

    class CollapsingHeader(private val maxPx: Float) : NestedScrollConnection {  var offset by mutableFloatStateOf(0f)          // -maxPx..0f    private set  // finger up: the header shrinks BEFORE the list scrolls  override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {    if (available.y >= 0f) return Offset.Zero…
  23. 23

    How do you animate a layout change where items move to new positions, for example a list switching to a two-column grid?

    Keep one layout whose parameters change, wrap it in a LookaheadScope, and put Modifier.animateBounds(lookaheadScope) on each item: the lookahead pass computes where every item is going, and each one animates its size and position towards that target.

    @OptIn(ExperimentalSharedTransitionApi::class, ExperimentalLayoutApi::class)@Composablefun Gallery(items: List<Item>, grid: Boolean) {  LookaheadScope {    FlowRow(      Modifier.animateContentSize(),                        // the container size changes too…
  24. 24

    How do you blur a composable or apply a custom shader, and what do you ship on Android versions that cannot do it?

    Modifier.blur works only on Android 12 (API 31) and newer, and AGSL RuntimeShader only on Android 13 (API 33); below that blur is a silent no-op, so design the fallback first (a gradient or a translucent scrim) and add the effect on top.

    @Composablefun CoverImage(modifier: Modifier = Modifier) {  // fallback decided in one place  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {   // API 31+    Box(modifier.blur(16.dp, BlurredEdgeTreatment.Unbounded)) { Cover() }  } else {…