Animation
Hướng dẫn tạo animations mượt mà trong Compose Multiplatform.
animateXAsState - Animation đơn giản
Phần tiêu đề “animateXAsState - Animation đơn giản”animateFloatAsState
Phần tiêu đề “animateFloatAsState”@Composablefun 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
Phần tiêu đề “animateColorAsState”@Composablefun 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
Phần tiêu đề “animateDpAsState”@Composablefun 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
Phần tiêu đề “AnimationSpec Types”// Tween - Duration-basedtween( durationMillis = 300, delayMillis = 0, easing = FastOutSlowInEasing // hoặc LinearEasing, LinearOutSlowInEasing)
// Spring - Physics-basedspring( dampingRatio = Spring.DampingRatioMediumBouncy, // 0.5f = bouncy stiffness = Spring.StiffnessLow // 200f = slow)
// Keyframeskeyframes { durationMillis = 1000 0f at 0 with LinearEasing 0.5f at 500 with FastOutSlowInEasing 1f at 1000}
// Repeatablerepeatable( iterations = 3, animation = tween(500), repeatMode = RepeatMode.Reverse)
// Infinite repeatableinfiniteRepeatable( animation = tween(1000), repeatMode = RepeatMode.Reverse)AnimatedVisibility
Phần tiêu đề “AnimatedVisibility”@Composablefun 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
Phần tiêu đề “Enter/Exit Animations”// FadefadeIn(animationSpec = tween(300))fadeOut(animationSpec = tween(300))
// SlideslideInHorizontally { fullWidth -> fullWidth } // from rightslideOutHorizontally { fullWidth -> -fullWidth } // to leftslideInVertically { fullHeight -> -fullHeight } // from topslideOutVertically { fullHeight -> fullHeight } // to bottom
// Expand/ShrinkexpandVertically()shrinkVertically()expandHorizontally()shrinkHorizontally()
// ScalescaleIn(initialScale = 0.5f)scaleOut(targetScale = 0.5f)
// CombinefadeIn() + slideInHorizontally()fadeOut() + slideOutHorizontally()AnimatedContent
Phần tiêu đề “AnimatedContent”@Composablefun 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
Phần tiêu đề “Crossfade”@Composablefun 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
Phần tiêu đề “Infinite Animations”Pulsing animation
Phần tiêu đề “Pulsing animation”@Composablefun 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
Phần tiêu đề “Rotating animation”@Composablefun 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
Phần tiêu đề “Shimmer effect”@Composablefun 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@Composablefun 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
Phần tiêu đề “Gesture Animations”Swipe to dismiss
Phần tiêu đề “Swipe to dismiss”@OptIn(ExperimentalMaterial3Api::class)@Composablefun 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
Phần tiêu đề “📝 Tóm tắt”| API | Mục đích |
|---|---|
animateXAsState |
Animate single value |
AnimatedVisibility |
Show/hide với animation |
AnimatedContent |
Content change animation |
Crossfade |
Fade between content |
rememberInfiniteTransition |
Looping animations |
AnimationSpec
Phần tiêu đề “AnimationSpec”| Type | Use case |
|---|---|
tween |
Time-based, easing |
spring |
Physics-based, bouncy |
keyframes |
Multiple steps |
repeatable |
Limited loops |
infiniteRepeatable |
Infinite loops |
Tiếp theo
Phần tiêu đề “Tiếp theo”Học về Bottom Sheet.