Animation
Animation làm cho ứng dụng trở nên sống động và tăng trải nghiệm người dùng. Compose cung cấp nhiều APIs mạnh mẽ để tạo animations một cách dễ dàng.
1. animate*AsState - Animation đơn giản nhất
Phần tiêu đề “1. animate*AsState - Animation đơn giản nhất”Cách sử dụng
Phần tiêu đề “Cách sử dụng”Thay đổi giá trị state → Compose tự động animate:
@Composablefun ExpandingBox() { var expanded by remember { mutableStateOf(false) }
// Tự động animate khi expanded thay đổi val size by animateDpAsState( targetValue = if (expanded) 200.dp else 100.dp, label = "size" )
Box( modifier = Modifier .size(size) .background(Color.Blue) .clickable { expanded = !expanded } )}Các variants
Phần tiêu đề “Các variants”// Animate Dpval size by animateDpAsState(targetValue = if (big) 200.dp else 100.dp)
// Animate Colorval color by animateColorAsState(targetValue = if (selected) Color.Red else Color.Gray)
// Animate Floatval alpha by animateFloatAsState(targetValue = if (visible) 1f else 0f)
// Animate Intval count by animateIntAsState(targetValue = targetCount)
// Animate Offsetval offset by animateOffsetAsState(targetValue = Offset(x, y))Custom AnimationSpec
Phần tiêu đề “Custom AnimationSpec”val size by animateDpAsState( targetValue = if (expanded) 200.dp else 100.dp, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ), label = "size")
val color by animateColorAsState( targetValue = targetColor, animationSpec = tween(durationMillis = 500, easing = LinearEasing), label = "color")2. AnimatedVisibility - Hiển thị/ẩn với animation
Phần tiêu đề “2. AnimatedVisibility - Hiển thị/ẩn với animation”Cơ bản
Phần tiêu đề “Cơ bản”@Composablefun ToastMessage(visible: Boolean, message: String) { AnimatedVisibility(visible = visible) { Card(modifier = Modifier.padding(16.dp)) { Text(message, modifier = Modifier.padding(16.dp)) } }}Custom Enter/Exit Transitions
Phần tiêu đề “Custom Enter/Exit Transitions”@Composablefun SlideInContent(visible: Boolean) { AnimatedVisibility( visible = visible, enter = fadeIn(animationSpec = tween(300)) + slideInVertically(initialOffsetY = { -it }), exit = fadeOut(animationSpec = tween(300)) + slideOutVertically(targetOffsetY = { -it }) ) { Card { Text("Sliding Content") } }}Các EnterTransition
Phần tiêu đề “Các EnterTransition”| Transition | Mô tả |
|---|---|
fadeIn() |
Fade từ transparent |
slideInHorizontally() |
Slide từ trái/phải |
slideInVertically() |
Slide từ trên/dưới |
scaleIn() |
Scale từ nhỏ |
expandIn() |
Expand từ một góc |
expandHorizontally() |
Expand theo chiều ngang |
expandVertically() |
Expand theo chiều dọc |
Animate children riêng biệt
Phần tiêu đề “Animate children riêng biệt”@Composablefun AnimateChildren(visible: Boolean) { AnimatedVisibility( visible = visible, enter = fadeIn(), exit = fadeOut() ) { Column { Text( "Title", modifier = Modifier.animateEnterExit( enter = slideInVertically { -it }, exit = slideOutVertically { -it } ) ) Text( "Subtitle", modifier = Modifier.animateEnterExit( enter = slideInVertically { it }, exit = slideOutVertically { it } ) ) } }}3. AnimatedContent - Chuyển đổi nội dung
Phần tiêu đề “3. AnimatedContent - Chuyển đổi nội dung”Cơ bản
Phần tiêu đề “Cơ bản”@Composablefun CounterAnimation(count: Int) { AnimatedContent( targetState = count, label = "counter" ) { targetCount -> Text( text = "$targetCount", style = MaterialTheme.typography.displayLarge ) }}Custom Transition
Phần tiêu đề “Custom Transition”@Composablefun TabContent(selectedTab: Int) { AnimatedContent( targetState = selectedTab, transitionSpec = { if (targetState > initialState) { // Slide left khi chuyển sang tab bên phải slideInHorizontally { it } + fadeIn() togetherWith slideOutHorizontally { -it } + fadeOut() } else { // Slide right khi chuyển sang tab bên trái slideInHorizontally { -it } + fadeIn() togetherWith slideOutHorizontally { it } + fadeOut() }.using(SizeTransform(clip = false)) }, label = "tab" ) { tab -> when (tab) { 0 -> HomeTab() 1 -> SearchTab() 2 -> ProfileTab() } }}4. Crossfade - Fade chuyển đổi
Phần tiêu đề “4. Crossfade - Fade chuyển đổi”@Composablefun LoadingState(isLoading: Boolean) { Crossfade( targetState = isLoading, animationSpec = tween(500), label = "loading" ) { loading -> if (loading) { CircularProgressIndicator() } else { Text("Content loaded!") } }}5. updateTransition - Animation phức tạp
Phần tiêu đề “5. updateTransition - Animation phức tạp”Khi bạn cần animate nhiều properties cùng lúc:
enum class ButtonState { Idle, Pressed }
@Composablefun AnimatedButton() { var buttonState by remember { mutableStateOf(ButtonState.Idle) }
val transition = updateTransition(targetState = buttonState, label = "button")
val scale by transition.animateFloat(label = "scale") { state -> when (state) { ButtonState.Idle -> 1f ButtonState.Pressed -> 0.95f } }
val backgroundColor by transition.animateColor(label = "background") { state -> when (state) { ButtonState.Idle -> Color.Blue ButtonState.Pressed -> Color.DarkGray } }
val borderWidth by transition.animateDp(label = "border") { state -> when (state) { ButtonState.Idle -> 0.dp ButtonState.Pressed -> 2.dp } }
Box( modifier = Modifier .scale(scale) .background(backgroundColor, RoundedCornerShape(8.dp)) .border(borderWidth, Color.White, RoundedCornerShape(8.dp)) .padding(16.dp) .pointerInput(Unit) { detectTapGestures( onPress = { buttonState = ButtonState.Pressed tryAwaitRelease() buttonState = ButtonState.Idle } ) } ) { Text("Press Me", color = Color.White) }}6. rememberInfiniteTransition - Animation vô hạn
Phần tiêu đề “6. rememberInfiniteTransition - Animation vô hạn”@Composablefun PulsingDot() { val infiniteTransition = rememberInfiniteTransition(label = "pulse")
val scale by infiniteTransition.animateFloat( initialValue = 1f, targetValue = 1.5f, animationSpec = infiniteRepeatable( animation = tween(1000), repeatMode = RepeatMode.Reverse ), label = "scale" )
val alpha by infiniteTransition.animateFloat( initialValue = 1f, targetValue = 0.3f, animationSpec = infiniteRepeatable( animation = tween(1000), repeatMode = RepeatMode.Reverse ), label = "alpha" )
Box( modifier = Modifier .size(50.dp) .scale(scale) .alpha(alpha) .background(Color.Red, CircleShape) )}Loading Shimmer Effect
Phần tiêu đề “Loading Shimmer Effect”@Composablefun ShimmerEffect() { val transition = rememberInfiniteTransition(label = "shimmer")
val translateX by transition.animateFloat( initialValue = -300f, targetValue = 300f, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing) ), label = "shimmer" )
Box( modifier = Modifier .fillMaxWidth() .height(100.dp) .background( brush = Brush.horizontalGradient( colors = listOf( Color.LightGray, Color.White, Color.LightGray ), startX = translateX, endX = translateX + 200f ) ) )}7. Animatable - Low-level Control
Phần tiêu đề “7. Animatable - Low-level Control”Khi cần kiểm soát animation hoàn toàn:
@Composablefun DraggableCard() { val offsetX = remember { Animatable(0f) }
Box( modifier = Modifier .offset { IntOffset(offsetX.value.roundToInt(), 0) } .pointerInput(Unit) { detectHorizontalDragGestures( onDragEnd = { // Spring back to original position launch { offsetX.animateTo( targetValue = 0f, animationSpec = spring(stiffness = Spring.StiffnessLow) ) } } ) { _, dragAmount -> launch { offsetX.snapTo(offsetX.value + dragAmount) } } } .size(100.dp) .background(Color.Blue, RoundedCornerShape(8.dp)) )}Animate to/SnapTo/Stop
Phần tiêu đề “Animate to/SnapTo/Stop”val animatable = remember { Animatable(0f) }
// Animate với animation specanimatable.animateTo(100f, animationSpec = tween(500))
// Jump trực tiếp đến giá trịanimatable.snapTo(100f)
// Dừng animationanimatable.stop()
// Animate với velocity (như fling)animatable.animateDecay(initialVelocity = 1000f, animationSpec = exponentialDecay())8. AnimationSpec Types
Phần tiêu đề “8. AnimationSpec Types”tween - Easing-based
Phần tiêu đề “tween - Easing-based”tween<Float>( durationMillis = 300, delayMillis = 50, easing = FastOutSlowInEasing)spring - Physics-based
Phần tiêu đề “spring - Physics-based”spring<Float>( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow)keyframes - Multi-step
Phần tiêu đề “keyframes - Multi-step”keyframes<Float> { durationMillis = 1000 0f at 0 with LinearEasing 0.5f at 200 with FastOutLinearInEasing 1f at 1000}repeatable / infiniteRepeatable
Phần tiêu đề “repeatable / infiniteRepeatable”repeatable<Float>( iterations = 3, animation = tween(500), repeatMode = RepeatMode.Reverse)
infiniteRepeatable<Float>( animation = tween(500), repeatMode = RepeatMode.Restart)9. Gesture-based Animation
Phần tiêu đề “9. Gesture-based Animation”@Composablefun SwipeableCard() { var offset by remember { mutableStateOf(0f) } var dismissed by remember { mutableStateOf(false) }
val animatedOffset by animateFloatAsState( targetValue = if (dismissed) 1000f else offset, animationSpec = spring(stiffness = Spring.StiffnessLow), finishedListener = { if (dismissed) { // Handle dismiss } }, label = "swipe" )
Box( modifier = Modifier .offset { IntOffset(animatedOffset.roundToInt(), 0) } .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> offset += delta }, onDragStopped = { velocity -> if (abs(offset) > 200 || abs(velocity) > 1000) { dismissed = true } else { offset = 0f } } ) .size(200.dp) .background(Color.Blue, RoundedCornerShape(16.dp)) )}10. Navigation Animation
Phần tiêu đề “10. Navigation Animation”@Composablefun AnimatedNavHost() { val navController = rememberNavController()
NavHost( navController = navController, startDestination = "home" ) { composable( route = "home", enterTransition = { fadeIn(tween(300)) }, exitTransition = { fadeOut(tween(300)) } ) { HomeScreen() }
composable( route = "detail", enterTransition = { slideInHorizontally(initialOffsetX = { it }) + fadeIn() }, exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) + fadeOut() }, popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) + fadeIn() }, popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) + fadeOut() } ) { DetailScreen() } }}📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”| API | Mục đích |
|---|---|
animate*AsState |
Animation đơn giản |
AnimatedVisibility |
Hiện/ẩn với animation |
AnimatedContent |
Chuyển đổi nội dung |
Crossfade |
Fade transition |
updateTransition |
Animation phức tạp nhiều properties |
rememberInfiniteTransition |
Animation vô hạn |
Animatable |
Low-level control |
Chọn API nào?
Phần tiêu đề “Chọn API nào?”Cần animate gì?├── Một giá trị đơn lẻ → animate*AsState├── Visibility → AnimatedVisibility├── Content switch → AnimatedContent hoặc Crossfade├── Nhiều properties cùng lúc → updateTransition├── Infinite loop → rememberInfiniteTransition└── Full control → Animatable