DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Jetpack Compose Animations: 4 Techniques to Make Your App Feel Alive

Jetpack Compose Animations: 4 Techniques to Make Your App Feel Alive

Animation is the heartbeat of modern Android apps. It transforms static UIs into fluid, responsive experiences that feel natural and delightful to users. Jetpack Compose, Google's modern declarative UI framework for Android, provides powerful built-in APIs to create smooth animations with just a few lines of code.

In this guide, I'll walk you through four essential animation techniques in Compose that will elevate your app's feel from ordinary to exceptional. Each technique comes with practical code examples you can use immediately in your projects.

1. animateFloatAsState: The Foundation of Smooth Value Changes

animateFloatAsState is perhaps the most commonly used animation API in Compose. It smoothly animates a float value from its current state to a target value, perfect for opacity fades, scale changes, and rotations.

Use Case: Animated Button Opacity on Tap

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color

@Composable
fun AnimatedOpacityButton() {
    var isPressed by remember { mutableStateOf(false) }

    // Animate opacity from 1.0 to 0.5 when pressed
    val alpha by animateFloatAsState(
        targetValue = if (isPressed) 0.5f else 1.0f,
        label = "button_alpha"
    )

    Button(
        onClick = { isPressed = !isPressed },
        modifier = Modifier.alpha(alpha)
    ) {
        Text("Tap me")
    }
}
Enter fullscreen mode Exit fullscreen mode

Why It Works

  • Smooth Transitions: The animation runs over 300ms by default, creating a natural fade effect
  • State-Driven: The animation automatically triggers whenever the state condition changes
  • Performance: Uses low-level graphics APIs for buttery-smooth 60fps animations

Customizing Animation Speed

You can control animation duration using animationSpec:

val alpha by animateFloatAsState(
    targetValue = if (isPressed) 0.5f else 1.0f,
    animationSpec = tween(durationMillis = 500), // Slower fade
    label = "button_alpha"
)
Enter fullscreen mode Exit fullscreen mode

2. AnimatedVisibility: Show/Hide with Polish

While animateFloatAsState handles value changes, AnimatedVisibility takes composables in and out of the view hierarchy with elegant enter/exit animations.

Use Case: Animated Error Message

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun LoginForm() {
    var showError by remember { mutableStateOf(false) }
    var email by remember { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") }
        )

        // Error message with slide + fade animation
        AnimatedVisibility(
            visible = showError,
            enter = slideInVertically() + fadeIn(),
            exit = slideOutVertically() + fadeOut()
        ) {
            Text(
                "Invalid email format",
                color = Color.Red,
                modifier = Modifier
                    .background(Color(0xFFFFEBEE))
                    .padding(12.dp)
            )
        }

        Button(onClick = {
            showError = email.isEmpty() || !email.contains("@")
        }) {
            Text("Sign In")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Animation Combinations

Compose lets you combine multiple animations:

enter = slideInVertically() + fadeIn() + expandVertically()
exit = slideOutVertically() + fadeOut() + shrinkVertically()
Enter fullscreen mode Exit fullscreen mode

This creates a polished effect where the error message slides in, fades in, and expands all at once.

3. animateContentSize: Smooth Layout Changes

When your composable's size changes due to content updates, animateContentSize smoothly animates the layout change instead of jumping instantly.

Use Case: Expanding Description Text

import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp

@Composable
fun ExpandableText(title: String, fullText: String) {
    var isExpanded by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .clickable { isExpanded = !isExpanded }
            .animateContentSize(
                animationSpec = tween(durationMillis = 400)
            )
            .padding(16.dp)
    ) {
        Text(
            text = title,
            style = MaterialTheme.typography.headlineSmall
        )

        Text(
            text = fullText,
            maxLines = if (isExpanded) Int.MAX_VALUE else 3,
            overflow = TextOverflow.Ellipsis
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

The Magic

When maxLines changes from 3 to MAX_VALUE, the column's height expands smoothly. No jarring jumps—just elegant growth.

4. updateTransition + Crossfade: Complex State Animations

For more complex animations involving multiple properties changing together, updateTransition orchestrates all animations as a single logical unit.

Use Case: Loading State with Spinner Rotation

import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

enum class LoadingState {
    Idle, Loading, Success, Error
}

@Composable
fun LoadingIndicator(state: LoadingState) {
    val transition = updateTransition(targetState = state, label = "loading_transition")

    val rotation by transition.animateFloat(label = "rotation") {
        if (it == LoadingState.Loading) 360f else 0f
    }

    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Crossfade(targetState = state, label = "content_crossfade") { loadingState ->
            when (loadingState) {
                LoadingState.Idle -> {
                    Text("Ready to load")
                }
                LoadingState.Loading -> {
                    CircularProgressIndicator(
                        modifier = Modifier
                            .size(40.dp)
                            .rotate(rotation)
                    )
                }
                LoadingState.Success -> {
                    Text("Loaded!", color = Color.Green)
                }
                LoadingState.Error -> {
                    Text("Error occurred", color = Color.Red)
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Why updateTransition is Powerful

  • Orchestration: All animations tied to one state change happen in sync
  • Consistency: Prevents animations from fighting each other
  • Performance: Recomposes only when animation values change, not on every frame

Bonus: Crossfade for Elegant Content Switching

Crossfade animates the transition between two different composables by fading the old one out while the new one fades in.

@Composable
fun TabContent(selectedTab: Int) {
    Crossfade(targetState = selectedTab, label = "tab_switch") { tab ->
        when (tab) {
            0 -> HomeScreen()
            1 -> ProfileScreen()
            else -> SettingsScreen()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

No complex enter/exit logic needed—Crossfade handles the visual elegance automatically.

Performance Tips

  1. Use label parameter: Helps with debugging in Android Studio's animation inspector
  2. Prefer animateFloatAsState for simple values: It's optimized and lightweight
  3. Avoid animating during initial composition: State must be stable before animation starts
  4. Test on real devices: Emulators don't accurately represent animation performance

Bringing It All Together

The most polished apps combine these techniques:

  • animateFloatAsState for quick state feedback (button presses, toggles)
  • AnimatedVisibility for entering/exiting screens and dialogs
  • animateContentSize for organic content growth (expanding lists, text)
  • updateTransition for complex, synchronized state changes

Each technique solves a specific animation problem, and mastering all four will make your Compose apps feel genuinely delightful.

Next Steps

All 8 templates use clean Compose UI ready for animations. https://myougatheax.gumroad.com

Start by implementing animateFloatAsState in your next feature, then gradually explore the other techniques as your app's complexity grows. Happy animating!

Top comments (0)