Jetpack Compose has revolutionized UI development on Android, and with Compose Multiplatform, that power extends to other platforms like iOS, desktop, and web. One of the key elements of engaging UIs is animation, and Compose Multiplatform provides robust tools to create visually appealing and performant animations across different platforms.
What are Compose Multiplatform Animations?
Compose Multiplatform Animations involve using Jetpack Compose’s animation APIs in a way that they work seamlessly across different platforms supported by Compose Multiplatform. This means animations defined once can be used on Android, iOS, desktop (JVM), and the web without platform-specific modifications.
Why Use Animations?
- Enhance User Experience: Animations make the UI feel more interactive and responsive.
- Provide Visual Feedback: Help users understand state changes and guide them through the UI.
- Improve App Polish: A well-animated app looks more professional and refined.
Core Concepts of Jetpack Compose Animations
Before diving into Multiplatform animations, let’s review some key Compose animation concepts:
1. AnimatedVisibility
This is used for animating the appearance and disappearance of composables.
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
@Composable
fun VisibilityAnimation() {
var isVisible by remember { mutableStateOf(false) }
Button(onClick = { isVisible = !isVisible }) {
Text(text = "Toggle Visibility")
}
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(),
exit = fadeOut()
) {
Text(text = "This text is animated.")
}
}
2. animate*AsState
Functions
Functions like animateFloatAsState
, animateColorAsState
, etc., are used for smoothly animating property changes.
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun ColorAnimation() {
var isRed by remember { mutableStateOf(false) }
val animatedColor by animateColorAsState(
targetValue = if (isRed) Color.Red else Color.Blue,
animationSpec = tween(durationMillis = 500, easing = LinearEasing)
)
Button(onClick = { isRed = !isRed }) {
Text(text = "Toggle Color")
}
Box(
modifier = Modifier
.size(100.dp)
.background(animatedColor)
)
}
3. Transition
API
For more complex animations that involve multiple states, the Transition
API is highly effective.
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.*
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
enum class BoxState {
SMALL,
LARGE
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun SizeAnimation() {
var boxState by remember { mutableStateOf(BoxState.SMALL) }
val transition = updateTransition(targetState = boxState, label = "boxSizeTransition")
val size by transition.animateDp(
transitionSpec = {
tween(durationMillis = 500)
}, label = "boxSize"
) { state ->
when (state) {
BoxState.SMALL -> 50.dp
BoxState.LARGE -> 150.dp
}
}
val buttonText by remember {
derivedStateOf {
if (boxState == BoxState.SMALL) "Make Large" else "Make Small"
}
}
Column(horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()) {
Spacer(Modifier.height(50.dp))
Button(onClick = {
boxState = when (boxState) {
BoxState.SMALL -> BoxState.LARGE
BoxState.LARGE -> BoxState.SMALL
}
}) {
Text(text = buttonText)
}
Spacer(Modifier.height(20.dp))
androidx.compose.foundation.Box(
modifier = Modifier
.size(size)
.background(androidx.compose.ui.graphics.Color.Green)
)
}
}
Implementing Multiplatform Animations
To implement animations that work across multiple platforms, focus on using Compose’s animation APIs in a way that doesn’t rely on platform-specific features.
1. Using Common Code
Put animation logic in your common code module. This ensures that the animation code is shared between all platforms.
// In commonMain
import androidx.compose.animation.core.*
import androidx.compose.runtime.*
enum class AnimationState {
START,
END
}
@Composable
fun rememberMultiplatformAnimation(): State {
val animationState = remember { mutableStateOf(AnimationState.START) }
val animatedProgress by animateFloatAsState(
targetValue = if (animationState.value == AnimationState.END) 1f else 0f,
animationSpec = tween(durationMillis = 1000, easing = LinearEasing),
label = "multiplatformAnimation"
)
LaunchedEffect(Unit) {
animationState.value = AnimationState.END
}
return animatedProgress.collectAsState()
}
2. Platform-Agnostic Properties
Stick to animating properties that are universally supported. Common properties include size, color, alpha, and position.
3. Example Animation
Let’s create a simple fading animation that can be used in a Multiplatform project.
// In commonMain
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun FadingText(isVisible: Boolean) {
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(animationSpec = tween(durationMillis = 1000)),
exit = fadeOut(animationSpec = tween(durationMillis = 1000))
) {
Text("Hello, Multiplatform!", modifier = Modifier.padding(16.dp))
}
}
@Composable
fun AnimationExample() {
var isVisible by remember { mutableStateOf(true) }
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(onClick = { isVisible = !isVisible }) {
Text("Toggle Visibility")
}
Spacer(modifier = Modifier.height(16.dp))
FadingText(isVisible = isVisible)
}
}
4. Platform-Specific Implementations (If Necessary)
Sometimes, minor tweaks may be needed for specific platforms. Use expect
/actual
declarations to handle platform-specific code if absolutely necessary.
// In commonMain
expect fun platformSpecificAnimation(): @Composable () -> Unit
// In androidMain
actual fun platformSpecificAnimation(): @Composable () -> Unit = {
Text("Android Specific Animation")
}
// In iosMain
actual fun platformSpecificAnimation(): @Composable () -> Unit = {
Text("iOS Specific Animation")
}
Example Project Setup
Create a new Compose Multiplatform project using the Kotlin Multiplatform wizard in IntelliJ IDEA.
- Project Name: ComposeAnimationExample
- Platforms: Android, iOS, Desktop
Add the shared animation code in the commonMain
source set and then use it across the platforms.
Best Practices
- Keep Animations Simple: Complex animations can be resource-intensive and may not perform well on all platforms.
- Test on Multiple Platforms: Always test animations on all target platforms to ensure they look and perform as expected.
- Use Standard Animation APIs: Stick to Compose’s standard animation APIs for maximum compatibility.
- Optimize for Performance: Use tools like the Compose Profiler to identify and fix performance bottlenecks.
Conclusion
Compose Multiplatform animations offer a powerful way to enhance the user experience across various platforms with a single codebase. By understanding the core animation concepts in Jetpack Compose and adhering to best practices, you can create engaging and performant UIs that delight users on Android, iOS, desktop, and beyond.