Remember the first time you saw a slick animation in an app and thought, “Wow, that’s cool”? That moment of delight is what we’re after when creating animations in our apps. With Compose Multiplatform, we can now bring those magical moments to users across Android, iOS, desktop, and web—all from a single codebase.

I’ve spent the last two years building cross-platform apps with Compose Multiplatform, and animations have consistently been the element that transforms good apps into great ones. They’re not just eye candy; they guide users, provide feedback, and make interactions feel natural and responsive.

Let’s dive into how you can create advanced animations in Compose Multiplatform that will make your users say “wow” regardless of which platform they’re on.

Getting Started with Compose Multiplatform Animations

Before we jump into the fancy stuff, let’s make sure we’re on the same page with the basics. Compose Multiplatform uses the same animation system as Jetpack Compose, with a few platform-specific considerations.

First, add the necessary dependencies to your build.gradle.kts file:

kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation("org.jetbrains.compose.animation:animation:1.5.0")
            }
        }
    }
}Code language: JavaScript (javascript)

Now, let’s look at the three main types of animations in Compose Multiplatform:

  1. State-based animations: Animate changes between different states
  2. Content-based animations: Animate the appearance and disappearance of content
  3. Low-level animations: Custom animations using the Animation API

State-Based Animations: The Building Blocks

State-based animations are the bread and butter of Compose animations. They’re easy to implement and cover most use cases.

Here’s a simple example of a button that grows when pressed:

@Composable
fun AnimatedButton(onClick: () -> Unit) {
    var pressed by remember { mutableStateOf(false) }

    val scale by animateFloatAsState(
        targetValue = if (pressed) 1.2f else 1f,
        label = "button scale"
    )

    Box(
        modifier = Modifier
            .scale(scale)
            .background(Color.Blue, RoundedCornerShape(8.dp))
            .clickable {
                pressed = !pressed
                onClick()
            }
            .padding(16.dp),
        contentAlignment = Alignment.Center
    ) {
        Text("Click Me", color = Color.White)
    }
}Code language: JavaScript (javascript)

This works across all platforms without any platform-specific code. The button smoothly scales up when pressed and back down when released.

But let’s kick it up a notch with multiple animated properties:

@Composable
fun FancyButton(onClick: () -> Unit) {
    var pressed by remember { mutableStateOf(false) }

    val scale by animateFloatAsState(
        targetValue = if (pressed) 1.2f else 1f,
        label = "button scale"
    )

    val color by animateColorAsState(
        targetValue = if (pressed) Color.Purple else Color.Blue,
        label = "button color"
    )

    val cornerRadius by animateDpAsState(
        targetValue = if (pressed) 16.dp else 8.dp,
        label = "corner radius"
    )

    Box(
        modifier = Modifier
            .scale(scale)
            .background(color, RoundedCornerShape(cornerRadius))
            .clickable {
                pressed = !pressed
                onClick()
            }
            .padding(16.dp),
        contentAlignment = Alignment.Center
    ) {
        Text("Fancy Button", color = Color.White)
    }
}Code language: JavaScript (javascript)

Now our button scales, changes color, AND adjusts its corner radius when pressed. That’s three animations running simultaneously with just a few lines of code!

Animating Lists and Collections

One of the most common animation needs is animating items in a list. Compose Multiplatform makes this surprisingly easy with AnimatedVisibility and animateItemPlacement().

Here’s how to create a list with animated item additions and removals:

@Composable
fun AnimatedShoppingList() {
    var items by remember { mutableStateOf(listOf("Apples", "Bananas", "Milk")) }
    var newItem by remember { mutableStateOf("") }

    Column {
        Row(
            modifier = Modifier.fillMaxWidth().padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            TextField(
                value = newItem,
                onValueChange = { newItem = it },
                modifier = Modifier.weight(1f),
                placeholder = { Text("Add item") }
            )

            Spacer(Modifier.width(8.dp))

            Button(onClick = {
                if (newItem.isNotEmpty()) {
                    items = items + newItem
                    newItem = ""
                }
            }) {
                Text("Add")
            }
        }

        LazyColumn {
            items(items, key = { it }) { item ->
                AnimatedVisibility(
                    visible = true,
                    enter = fadeIn() + expandVertically(),
                    exit = fadeOut() + shrinkVertically()
                ) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .animateItemPlacement()
                            .padding(16.dp),
                        horizontalArrangement = Arrangement.SpaceBetween,
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Text(item)

                        IconButton(onClick = { items = items - item }) {
                            Text("🗑️")
                        }
                    }
                }
            }
        }
    }
}Code language: PHP (php)

When you add or remove items, they’ll smoothly animate in and out of the list. The animateItemPlacement() modifier ensures that when items change position, they slide into place rather than jumping.

Custom Transitions Between Screens

Navigation animations can significantly improve the user experience. Let’s create a custom transition between two screens:

enum class Screen { List, Detail }

@Composable
fun NavigationExample() {
    var currentScreen by remember { mutableStateOf(Screen.List) }
    var selectedItem by remember { mutableStateOf<String?>(null) }

    AnimatedContent(
        targetState = currentScreen,
        transitionSpec = {
            when {
                targetState == Screen.Detail && initialState == Screen.List ->
                    slideInHorizontally { width -> width } with 
                    slideOutHorizontally { width -> -width }
                else ->
                    slideInHorizontally { width -> -width } with 
                    slideOutHorizontally { width -> width }
            }
        }
    ) { screen ->
        when (screen) {
            Screen.List -> {
                ItemList(
                    onItemClick = { item ->
                        selectedItem = item
                        currentScreen = Screen.Detail
                    }
                )
            }
            Screen.Detail -> {
                ItemDetail(
                    item = selectedItem ?: "",
                    onBackClick = {
                        currentScreen = Screen.List
                    }
                )
            }
        }
    }
}

@Composable
fun ItemList(onItemClick: (String) -> Unit) {
    // List implementation
}

@Composable
fun ItemDetail(item: String, onBackClick: () -> Unit) {
    // Detail implementation
}Code language: PHP (php)

This creates a slide animation when navigating between screens, similar to what you’d see in native apps. The direction of the slide depends on whether you’re going forward to the detail screen or back to the list.

Platform-Specific Considerations

While most animations work identically across platforms, there are some platform-specific considerations to keep in mind:

PlatformConsiderationSolution
iOSSome animations may feel too “Android-like”Adjust timing and easing to match iOS conventions
WebPerformance can be an issue with complex animationsSimplify animations or use LaunchedEffect for JS-specific optimizations
DesktopMouse interactions differ from touchAdd hover animations for better desktop UX

For iOS-specific animations, you might want to adjust the animation specs:

val duration = if (Platform.isIOS) 350 else 300
val easing = if (Platform.isIOS) FastOutSlowInEasing else LinearOutSlowInEasing

val animSpec = tween<Float>(
    durationMillis = duration,
    easing = easing
)

val scale by animateFloatAsState(
    targetValue = if (pressed) 1.2f else 1f,
    animationSpec = animSpec,
    label = "button scale"
)Code language: JavaScript (javascript)

Creating a Lottie-Like Animation System

For really complex animations, you might want something similar to Lottie. While there’s no official Lottie support for Compose Multiplatform yet, we can create our own animation system using Canvas and Animatable:

@Composable
fun ParticleExplosion(modifier: Modifier = Modifier) {
    val particles = remember { List(100) { Particle() } }
    val animatedProgress = remember { Animatable(0f) }

    LaunchedEffect(Unit) {
        animatedProgress.animateTo(
            targetValue = 1f,
            animationSpec = tween(2000, easing = LinearEasing)
        )
    }

    Canvas(modifier = modifier.size(200.dp)) {
        val progress = animatedProgress.value

        particles.forEach { particle ->
            val distance = particle.maxDistance * progress
            val x = size.width / 2 + cos(particle.angle) * distance
            val y = size.height / 2 + sin(particle.angle) * distance
            val alpha = 1f - progress

            drawCircle(
                color = particle.color.copy(alpha = alpha),
                radius = particle.size * (1f - progress * 0.5f),
                center = Offset(x, y)
            )
        }
    }
}

class Particle {
    val angle = Random.nextFloat() * 2 * PI.toFloat()
    val maxDistance = Random.nextFloat() * 500f + 50f
    val size = Random.nextFloat() * 10f + 5f
    val color = Color(
        red = Random.nextFloat(),
        green = Random.nextFloat(),
        blue = Random.nextFloat(),
        alpha = 1f
    )
}Code language: PHP (php)

This creates a particle explosion effect where particles fly outward from the center and fade away. You can customize this to create all sorts of complex animations.

Practical Example: Pull-to-Refresh Animation

Let’s create a practical example that you might use in a real app: a custom pull-to-refresh animation:

@Composable
fun CustomPullToRefresh(
    refreshing: Boolean,
    onRefresh: () -> Unit,
    content: @Composable () -> Unit
) {
    val refreshState = rememberPullRefreshState(
        refreshing = refreshing,
        onRefresh = onRefresh
    )

    Box(
        modifier = Modifier
            .fillMaxSize()
            .pullRefresh(refreshState)
    ) {
        content()

        PullRefreshIndicator(
            refreshing = refreshing,
            state = refreshState,
            modifier = Modifier.align(Alignment.TopCenter),
            backgroundColor = MaterialTheme.colorScheme.surface,
            contentColor = MaterialTheme.colorScheme.primary,
            scale = true
        )
    }
}

@Composable
fun NewsScreen() {
    var refreshing by remember { mutableStateOf(false) }
    var items by remember { mutableStateOf(List(20) { "News Item ${it + 1}" }) }

    LaunchedEffect(refreshing) {
        if (refreshing) {
            delay(2000) // Simulate network request
            items = List(20) { "Fresh News ${it + 1}" }
            refreshing = false
        }
    }

    CustomPullToRefresh(
        refreshing = refreshing,
        onRefresh = { refreshing = true }
    ) {
        LazyColumn {
            items(items) { item ->
                Text(
                    text = item,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(16.dp)
                )
                Divider()
            }
        }
    }
}Code language: PHP (php)

This creates a custom pull-to-refresh experience that works on both Android and iOS. The animation is smooth and provides good feedback to the user about what’s happening.

Performance Tips for Smooth Animations

Animations can be resource-intensive, especially on lower-end devices. Here are some tips to keep your animations running smoothly:

  1. Animate only what’s necessary: Don’t animate properties that don’t need to change
  2. Use remember and key: Avoid recreating animations on every recomposition
  3. Simplify during animation: Temporarily reduce complexity during animations
  4. Test on real devices: Emulators can be misleading for performance testing

Here’s an example of simplifying content during animation:

@Composable
fun OptimizedCardExpansion() {
    var expanded by remember { mutableStateOf(false) }
    val transition = updateTransition(expanded, label = "card expansion")

    val height by transition.animateDp(label = "height") { isExpanded ->
        if (isExpanded) 300.dp else 100.dp
    }

    val isAnimating = transition.isRunning

    Card(
        modifier = Modifier
            .fillMaxWidth()
            .height(height)
            .clickable { expanded = !expanded },
        elevation = CardDefaults.cardElevation(4.dp)
    ) {
        Box(Modifier.fillMaxSize()) {
            if (isAnimating) {
                // Show simplified content during animation
                Text(
                    "Loading...",
                    modifier = Modifier.align(Alignment.Center)
                )
            } else {
                // Show full content when not animating
                Column(Modifier.padding(16.dp)) {
                    Text("Card Title", style = MaterialTheme.typography.headlineSmall)

                    if (expanded) {
                        Spacer(Modifier.height(8.dp))
                        Text("This is the detailed content that appears when the card is expanded. It can contain complex layouts and even images.")
                        // Complex content here
                    }
                }
            }
        }
    }
}Code language: JavaScript (javascript)

This approach shows simplified content during the animation and then switches to the full content once the animation completes, ensuring smooth performance even with complex layouts.

Wrapping Up

Animations in Compose Multiplatform are powerful tools that can take your app from functional to fantastic. They guide users, provide feedback, and add that extra polish that makes apps feel professional and well-crafted.

We’ve covered state-based animations, content transitions, list animations, custom drawing animations, and practical examples like pull-to-refresh. With these techniques, you can create engaging, cross-platform experiences that delight users on any device.

Remember that the best animations are the ones users barely notice—they just make the app feel natural and responsive. So go ahead, add some animation magic to your Compose Multiplatform app, and watch as your users’ experience transforms from ordinary to extraordinary!

Happy animating!

0 0 votes
Article Rating

Leave a Reply

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments