Skip to Content

Animation trong Compose Multiplatform

Hướng dẫn tạo animations mượt mà trong Compose Multiplatform.

animateXAsState - Animation đơn giản

animateFloatAsState

@Composable fun ExpandingBox() { var expanded by remember { mutableStateOf(false) } val size by animateFloatAsState( targetValue = if (expanded) 200f else 100f, animationSpec = tween(durationMillis = 300) ) Box( modifier = Modifier .size(size.dp) .background(Color.Blue) .clickable { expanded = !expanded } ) }

animateColorAsState

@Composable fun ColorChangingBox() { var isRed by remember { mutableStateOf(true) } val color by animateColorAsState( targetValue = if (isRed) Color.Red else Color.Blue, animationSpec = tween(500) ) Box( modifier = Modifier .size(100.dp) .background(color) .clickable { isRed = !isRed } ) }

animateDpAsState

@Composable fun SlidingBox() { var isRight by remember { mutableStateOf(false) } val offset by animateDpAsState( targetValue = if (isRight) 200.dp else 0.dp, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ) ) Box( modifier = Modifier .offset(x = offset) .size(50.dp) .background(Color.Green, CircleShape) .clickable { isRight = !isRight } ) }

AnimationSpec Types

// Tween - Duration-based tween( durationMillis = 300, delayMillis = 0, easing = FastOutSlowInEasing // hoặc LinearEasing, LinearOutSlowInEasing ) // Spring - Physics-based spring( dampingRatio = Spring.DampingRatioMediumBouncy, // 0.5f = bouncy stiffness = Spring.StiffnessLow // 200f = slow ) // Keyframes keyframes { durationMillis = 1000 0f at 0 with LinearEasing 0.5f at 500 with FastOutSlowInEasing 1f at 1000 } // Repeatable repeatable( iterations = 3, animation = tween(500), repeatMode = RepeatMode.Reverse ) // Infinite repeatable infiniteRepeatable( animation = tween(1000), repeatMode = RepeatMode.Reverse )

AnimatedVisibility

@Composable fun AnimatedVisibilityExample() { var visible by remember { mutableStateOf(true) } Column { Button(onClick = { visible = !visible }) { Text(if (visible) "Hide" else "Show") } AnimatedVisibility( visible = visible, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { Card( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text("This content can be shown/hidden", Modifier.padding(16.dp)) } } } }

Enter/Exit Animations

// Fade fadeIn(animationSpec = tween(300)) fadeOut(animationSpec = tween(300)) // Slide slideInHorizontally { fullWidth -> fullWidth } // from right slideOutHorizontally { fullWidth -> -fullWidth } // to left slideInVertically { fullHeight -> -fullHeight } // from top slideOutVertically { fullHeight -> fullHeight } // to bottom // Expand/Shrink expandVertically() shrinkVertically() expandHorizontally() shrinkHorizontally() // Scale scaleIn(initialScale = 0.5f) scaleOut(targetScale = 0.5f) // Combine fadeIn() + slideInHorizontally() fadeOut() + slideOutHorizontally()

AnimatedContent

@Composable fun AnimatedCounter() { var count by remember { mutableStateOf(0) } Row(verticalAlignment = Alignment.CenterVertically) { IconButton(onClick = { count-- }) { Icon(Icons.Default.Remove, "Decrease") } AnimatedContent( targetState = count, transitionSpec = { if (targetState > initialState) { // Going up slideInVertically { -it } + fadeIn() togetherWith slideOutVertically { it } + fadeOut() } else { // Going down slideInVertically { it } + fadeIn() togetherWith slideOutVertically { -it } + fadeOut() }.using(SizeTransform(clip = false)) } ) { targetCount -> Text( text = "$targetCount", style = MaterialTheme.typography.displayMedium ) } IconButton(onClick = { count++ }) { Icon(Icons.Default.Add, "Increase") } } }

Crossfade

@Composable fun CrossfadeExample() { var currentScreen by remember { mutableStateOf("A") } Column { Row { Button(onClick = { currentScreen = "A" }) { Text("Screen A") } Button(onClick = { currentScreen = "B" }) { Text("Screen B") } } Crossfade(targetState = currentScreen, animationSpec = tween(500)) { screen -> when (screen) { "A" -> ScreenA() "B" -> ScreenB() } } } }

Infinite Animations

Pulsing animation

@Composable fun PulsingDot() { val infiniteTransition = rememberInfiniteTransition() val scale by infiniteTransition.animateFloat( initialValue = 1f, targetValue = 1.2f, animationSpec = infiniteRepeatable( animation = tween(500), repeatMode = RepeatMode.Reverse ) ) Box( modifier = Modifier .scale(scale) .size(20.dp) .background(Color.Red, CircleShape) ) }

Rotating animation

@Composable fun RotatingIcon() { val infiniteTransition = rememberInfiniteTransition() val rotation by infiniteTransition.animateFloat( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Restart ) ) Icon( Icons.Default.Refresh, contentDescription = "Loading", modifier = Modifier.rotate(rotation) ) }

Shimmer effect

@Composable fun ShimmerEffect( modifier: Modifier = Modifier ) { val infiniteTransition = rememberInfiniteTransition() val shimmerTranslate by infiniteTransition.animateFloat( initialValue = 0f, targetValue = 1000f, animationSpec = infiniteRepeatable( animation = tween(1200, easing = LinearEasing), repeatMode = RepeatMode.Restart ) ) val shimmerColors = listOf( Color.LightGray.copy(alpha = 0.6f), Color.LightGray.copy(alpha = 0.2f), Color.LightGray.copy(alpha = 0.6f) ) val brush = Brush.linearGradient( colors = shimmerColors, start = Offset(shimmerTranslate - 200, shimmerTranslate - 200), end = Offset(shimmerTranslate, shimmerTranslate) ) Box( modifier = modifier .background(brush, RoundedCornerShape(8.dp)) ) } // Skeleton loading @Composable fun SkeletonCard() { Card(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.padding(16.dp)) { ShimmerEffect( modifier = Modifier.size(48.dp) ) Spacer(Modifier.width(16.dp)) Column { ShimmerEffect( modifier = Modifier .height(16.dp) .fillMaxWidth(0.7f) ) Spacer(Modifier.height(8.dp)) ShimmerEffect( modifier = Modifier .height(12.dp) .fillMaxWidth(0.5f) ) } } } }

Gesture Animations

Swipe to dismiss

@OptIn(ExperimentalMaterial3Api::class) @Composable fun SwipeToDismissItem( item: String, onDismiss: () -> Unit ) { val dismissState = rememberSwipeToDismissBoxState( confirmValueChange = { if (it == SwipeToDismissBoxValue.EndToStart) { onDismiss() true } else { false } } ) SwipeToDismissBox( state = dismissState, backgroundContent = { Box( modifier = Modifier .fillMaxSize() .background(Color.Red) .padding(16.dp), contentAlignment = Alignment.CenterEnd ) { Icon(Icons.Default.Delete, "Delete", tint = Color.White) } } ) { Card(modifier = Modifier.fillMaxWidth()) { Text(item, modifier = Modifier.padding(16.dp)) } } }

📝 Tóm tắt

APIMục đích
animateXAsStateAnimate single value
AnimatedVisibilityShow/hide với animation
AnimatedContentContent change animation
CrossfadeFade between content
rememberInfiniteTransitionLooping animations

AnimationSpec

TypeUse case
tweenTime-based, easing
springPhysics-based, bouncy
keyframesMultiple steps
repeatableLimited loops
infiniteRepeatableInfinite loops

Tiếp theo

Học về Bottom Sheet.

Last updated on