CPS251 Android Development by Scott Shaper

Animations and Transitions in Material Design

Introduction

Animations and transitions are like the body language of your app—they help users understand what's happening, guide their attention, and make your app feel alive. In Material Design, motion isn't just for decoration; it helps users follow changes and makes your app more enjoyable to use. Jetpack Compose gives you easy tools to add smooth, meaningful animations to your UI, even if you've never done it before.

When to Use Animations and Transitions

  • To show changes in state (for example, expanding a card or switching tabs)
  • To guide the user's attention to important actions or new content
  • To make your app feel more polished and professional
  • To provide feedback (like a button press, loading spinner, or error message)

Common Animation Tools in Compose

ToolWhat It DoesWhen to Use It
animate*AsStateAnimates a value (like color, size, or position) smoothly from one state to anotherFor simple state changes, like making a button grow when pressed
AnimatedVisibilityAnimates showing or hiding content (fades, slides, etc.)For expanding/collapsing UI, like showing extra details
updateTransitionCoordinates multiple animations at onceFor more complex transitions, like animating several properties together
rememberInfiniteTransitionCreates looping or repeating animationsFor effects like pulsing, spinning, or loading indicators
CrossfadeAnimates switching between two composables with a fade effectFor smoothly changing screens or content
AnimatableGives you manual control to animate to specific values, interrupt, or chain animationsFor custom or interactive animations
AnimationSpecCustomizes the timing, speed, and feel of your animations (spring, tween, keyframes, etc.)When you want to fine-tune how your animation moves
TransitionAnimates multiple values together with more controlFor advanced, coordinated animations

Practical Example: Animated Card with Multiple Effects

@Composable
    fun AnimatedCardExample() {
        // State variables for animations
        var expanded by remember { mutableStateOf(false) }
        var showDetails by remember { mutableStateOf(false) }
    
        // Animated values
        val cardColor by animateColorAsState(
            targetValue = if (expanded) Color(0xFFBBDEFB) else Color.White,
            label = "cardColor"
        )
        val cardElevation by animateDpAsState(
            targetValue = if (expanded) 26.dp else 4.dp,
            label = "cardElevation"
        )
        val cardWidth by animateDpAsState(
            targetValue = if (expanded) 300.dp else 200.dp,
            label = "cardWidth"
        )
    
        // Card with animated properties
        Card(
            colors = CardDefaults.cardColors(
                containerColor = cardColor
            ),
            elevation = CardDefaults.cardElevation(
                defaultElevation = cardElevation
            ),
            modifier = Modifier
                .width(cardWidth)
                .padding(16.dp)
                .padding(top=50.dp)
                .clickable {
                    expanded = !expanded
                    showDetails = expanded
                }
        ) {
            Column(modifier = Modifier.padding(16.dp)) {
                Text(
                    text = "Tap to ${if (expanded) "collapse" else "expand"}",
                    style = MaterialTheme.typography.titleLarge
                )
    
                // Animated visibility for additional content
                AnimatedVisibility(visible = expanded) {
                    Column {
                        Spacer(modifier = Modifier.height(8.dp))
                        // Crossfade animation between states
                        Crossfade(targetState = showDetails) { detailsVisible ->
                            if (detailsVisible) {
                                Text("Here are more details! This text fades in.")
                            } else {
                                Text("Tap to see more details.")
                            }
                        }
                    }
                }
            }
        }
    }
   
    @Composable
    fun AnimatedBoxExample() {
        // State variable to track if box is expanded
        var isExpanded by remember { mutableStateOf(false) }
    
        // Animated size using animationSpec with tween()
        // tween() allows us to specify duration in milliseconds
        val boxSize by animateDpAsState(
            targetValue = if (isExpanded) 150.dp else 80.dp,
            animationSpec = tween(
                durationMillis = 500, // Animation duration: 500ms
                // easing = FastOutSlowInEasing (default) - starts fast, ends slow
            ),
            label = "boxSize"
        )
    
        // Animated color for visual feedback
        val boxColor by animateColorAsState(
            targetValue = if (isExpanded) Color(0xFF4CAF50) else Color(0xFF2196F3),
            animationSpec = tween(durationMillis = 500),
            label = "boxColor"
        )
    
        Column(
            modifier = Modifier.padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = "Tap the box to animate!",
                style = MaterialTheme.typography.titleMedium,
                modifier = Modifier.padding(bottom = 16.dp)
            )
    
            // Animated box
            Box(
                modifier = Modifier
                    .size(boxSize)
                    .background(
                        color = boxColor,
                        shape = RoundedCornerShape(12.dp)
                    )
                    .clickable {
                        isExpanded = !isExpanded
                    }
            )
    
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = if (isExpanded) "Tap to shrink" else "Tap to expand",
                style = MaterialTheme.typography.bodySmall
            )
        }
    }
  • What does this example do? This card animates its color, elevation (shadow), and width when you tap it. When expanded, it also reveals extra details with a fade-in effect using Crossfade and AnimatedVisibility.
  • How does it work? animateColorAsState, animateDpAsState, and AnimatedVisibility work together to animate the card's appearance and content. Crossfade smoothly switches between two pieces of text.
  • Why use this? Combining multiple animation tools makes your UI feel more dynamic and helps users understand what's changing. This example shows how you can animate several properties and content at once, all with simple Compose code.

How this example renders

Above is just a snippet of the code to view the full code, you need to go to my GitHub page and look at the chapter13 animation.kt file. When you run the code it will look like the example below.

Animation Example Animation Example

Tips for Success

  • Use animations to clarify what's happening, not just to decorate
  • Keep animations quick and smooth—most should finish in under 300ms
  • Test on real devices to make sure animations look good and don't slow things down
  • If you're not sure, start simple! Even a small animation can make a big difference

Common Mistakes to Avoid

  • Overusing animations so your app feels slow or distracting
  • Using animations that don't match Material Design guidelines (like odd timing or movement)
  • Making animations too long or too flashy—users want to get things done!
  • Forgetting to test on different devices (animations can look different on slow phones)

Best Practices

  • Use motion to guide and inform, not just to decorate
  • Follow Material Design's motion guidelines (see Google's docs for details)
  • Keep your animations consistent across the app—use the same timing and style everywhere
  • Comment your code to explain why you're animating something, especially if it's not obvious