# Jetpack Compose: Hoạt ảnh nâng cao từng bước > Hướng dẫn đầy đủ về hoạt ảnh nâng cao trong Compose: chuyển tiếp, AnimatedVisibility, Animatable, cử chỉ và hiệu năng cho giao diện Android mượt mà. - Published: 2026-02-08 - Updated: 2026-04-28 - Author: SharpSkill - Tags: jetpack compose, android, animations, kotlin, ui - Reading time: 12 min --- Hoạt ảnh biến một ứng dụng có chức năng thành một trải nghiệm đáng nhớ. Jetpack Compose cung cấp một API hoạt ảnh khai báo mạnh mẽ giúp đơn giản hóa đáng kể việc tạo các giao diện mượt mà. Hướng dẫn này khám phá các kỹ thuật nâng cao để xây dựng hoạt ảnh hiệu quả và dễ bảo trì. > **Yêu cầu trước tiên** > > Hướng dẫn này giả định rằng người đọc đã quen với các kiến thức nền tảng của Compose (recomposition, state, modifier). Để ôn lại kiến thức cơ bản, hãy tham khảo trước hướng dẫn câu hỏi phỏng vấn về Jetpack Compose. ## Nền tảng API Hoạt ảnh trong Compose Compose cung cấp nhiều cấp độ API cho hoạt ảnh. Lựa chọn phụ thuộc vào mức độ kiểm soát mong muốn và độ phức tạp của hoạt ảnh. API được chia thành ba danh mục chính: hoạt ảnh cấp cao (`AnimatedVisibility`, `AnimatedContent`), hoạt ảnh dựa trên state (`animate*AsState`) và hoạt ảnh cấp thấp (`Animatable`, `Transition`). ```kotlin // AnimationLevels.kt // Overview of the three animation API levels @Composable fun AnimationApiOverview() { // High level: simple predefined animations AnimatedVisibility(visible = isVisible) { Text("Animated content") } // Intermediate level: state-driven animation val alpha by animateFloatAsState( targetValue = if (isSelected) 1f else 0.5f, label = "alpha" ) // Low level: full control over animation val animatable = remember { Animatable(0f) } LaunchedEffect(targetValue) { animatable.animateTo(targetValue) } } ``` Việc lựa chọn cấp độ API phù hợp là yếu tố then chốt để giữ mã dễ đọc trong khi vẫn duy trì sự linh hoạt cần thiết. ## AnimatedVisibility: Hoạt ảnh xuất hiện và biến mất tinh tế `AnimatedVisibility` là điểm khởi đầu lý tưởng để tạo hoạt ảnh cho việc xuất hiện và biến mất của các phần tử. API này tự động quản lý việc tạo và hủy thành phần. Các tham số `enter` và `exit` chấp nhận các tổ hợp chuyển tiếp xác định hành vi của hoạt ảnh. Các chuyển tiếp này có thể được kết hợp bằng toán tử `+`. ```kotlin // AnimatedVisibilityExample.kt @Composable fun ExpandableCard( title: String, content: String, modifier: Modifier = Modifier ) { var isExpanded by remember { mutableStateOf(false) } Card( modifier = modifier.clickable { isExpanded = !isExpanded } ) { Column(modifier = Modifier.padding(16.dp)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text(text = title, style = MaterialTheme.typography.titleMedium) Icon( imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, contentDescription = null ) } // Expansion animation with fade + slide AnimatedVisibility( visible = isExpanded, enter = fadeIn(animationSpec = tween(300)) + expandVertically(animationSpec = tween(300)), exit = fadeOut(animationSpec = tween(200)) + shrinkVertically(animationSpec = tween(200)) ) { Text( text = content, modifier = Modifier.padding(top = 12.dp), style = MaterialTheme.typography.bodyMedium ) } } } } ``` Nội dung bên trong `AnimatedVisibility` chỉ được tạo khi `visible = true`, giúp tối ưu hóa hiệu năng cho các danh sách có nhiều phần tử có thể mở rộng. > **Kết hợp các chuyển tiếp** > > Các chuyển tiếp khả dụng bao gồm `fadeIn/fadeOut`, `slideIn/slideOut`, `expandIn/shrinkOut`, `scaleIn/scaleOut`. Chúng có thể được kết hợp tự do để tạo ra các hiệu ứng tùy chỉnh. ## animate*AsState: Hoạt ảnh dựa trên State Họ hàm `animate*AsState` tự động tạo hoạt ảnh cho các thay đổi của các giá trị nguyên thủy. Đây là cách tiếp cận đặc trưng nhất cho các hoạt ảnh đơn giản trong Compose. Mỗi kiểu dữ liệu có hàm chuyên dụng riêng: `animateColorAsState`, `animateFloatAsState`, `animateDpAsState`, `animateIntAsState`, v.v. ```kotlin // AnimateAsStateExample.kt @Composable fun InteractiveButton( isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier ) { // Animated background color val backgroundColor by animateColorAsState( targetValue = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, animationSpec = tween(durationMillis = 250), label = "backgroundColor" ) // Animated elevation val elevation by animateDpAsState( targetValue = if (isSelected) 8.dp else 2.dp, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ), label = "elevation" ) // Animated text size val textSize by animateFloatAsState( targetValue = if (isSelected) 18f else 14f, label = "textSize" ) Surface( modifier = modifier.clickable(onClick = onClick), color = backgroundColor, shadowElevation = elevation, shape = RoundedCornerShape(12.dp) ) { Text( text = if (isSelected) "Selected" else "Select", modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp), fontSize = textSize.sp ) } } ``` Tham số `animationSpec` kiểm soát hành vi thời gian của hoạt ảnh. Hai spec được sử dụng phổ biến nhất là `tween` (thời lượng cố định kèm easing) và `spring` (vật lý thực tế kèm hiệu ứng nảy). ## Transition: Điều phối nhiều hoạt ảnh Khi nhiều thuộc tính cần được tạo hoạt ảnh một cách phối hợp, `updateTransition` cung cấp khả năng kiểm soát tập trung. API này đảm bảo tất cả các hoạt ảnh luôn đồng bộ. Mẫu này bao gồm việc định nghĩa một state dạng enum, sau đó tạo hoạt ảnh cho mỗi thuộc tính phụ thuộc vào state đó. ```kotlin // TransitionExample.kt // Card state: defines visual behavior enum class CardState { Collapsed, Expanded, Selected } @Composable fun AnimatedStateCard( cardState: CardState, modifier: Modifier = Modifier ) { // Central transition coordinating all animations val transition = updateTransition( targetState = cardState, label = "cardTransition" ) // Card height based on state val cardHeight by transition.animateDp( transitionSpec = { spring(stiffness = Spring.StiffnessLow) }, label = "height" ) { state -> when (state) { CardState.Collapsed -> 80.dp CardState.Expanded -> 200.dp CardState.Selected -> 160.dp } } // Border color based on state val borderColor by transition.animateColor( transitionSpec = { tween(300) }, label = "borderColor" ) { state -> when (state) { CardState.Collapsed -> Color.Transparent CardState.Expanded -> MaterialTheme.colorScheme.outline CardState.Selected -> MaterialTheme.colorScheme.primary } } // Corner radius based on state val cornerRadius by transition.animateDp( label = "cornerRadius" ) { state -> when (state) { CardState.Collapsed -> 8.dp CardState.Expanded -> 16.dp CardState.Selected -> 24.dp } } Card( modifier = modifier .height(cardHeight) .border(2.dp, borderColor, RoundedCornerShape(cornerRadius)), shape = RoundedCornerShape(cornerRadius) ) { // Card content } } ``` ## Animatable: Kiểm soát toàn diện hoạt ảnh `Animatable` là API cấp thấp cung cấp khả năng kiểm soát lập trình hoàn toàn. Cách tiếp cận này cần thiết cho các hoạt ảnh có thể bị gián đoạn, các cử chỉ hoặc các kịch bản phức tạp. Khác với `animate*AsState`, `Animatable` cho phép dừng, đảo ngược hoặc chỉnh sửa một hoạt ảnh đang chạy mà không cần đợi nó kết thúc. ```kotlin // AnimatableExample.kt @Composable fun SwipeableCard( onDismiss: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit ) { // Horizontal offset controlled by Animatable val offsetX = remember { Animatable(0f) } val scope = rememberCoroutineScope() // Swipe threshold to trigger dismissal val dismissThreshold = 300f Box( modifier = modifier .offset { IntOffset(offsetX.value.roundToInt(), 0) } .pointerInput(Unit) { detectHorizontalDragGestures( onDragEnd = { scope.launch { if (abs(offsetX.value) > dismissThreshold) { // Animate out then callback val target = if (offsetX.value > 0) 1000f else -1000f offsetX.animateTo( targetValue = target, animationSpec = tween(200) ) onDismiss() } else { // Return to initial position with spring offsetX.animateTo( targetValue = 0f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy ) ) } } }, onHorizontalDrag = { _, dragAmount -> scope.launch { // snapTo for instant finger tracking offsetX.snapTo(offsetX.value + dragAmount) } } ) } ) { content() } } ``` Các phương thức quan trọng của `Animatable` là `animateTo()` (tạo hoạt ảnh đến mục tiêu), `snapTo()` (thay đổi tức thì) và `stop()` (gián đoạn). ## AnimatedContent: Chuyển tiếp nội dung `AnimatedContent` tạo hoạt ảnh cho các chuyển tiếp giữa các nội dung khác nhau. API này hoàn hảo cho những thay đổi state làm thay đổi hoàn toàn UI hiển thị. Khóa `targetState` xác định khi nào cần xảy ra chuyển tiếp. `transitionSpec` định nghĩa cách nội dung đi ra và đi vào tương tác với nhau. ```kotlin // AnimatedContentExample.kt @Composable fun CounterWithAnimation( count: Int, modifier: Modifier = Modifier ) { AnimatedContent( targetState = count, modifier = modifier, transitionSpec = { // Determine animation direction val direction = if (targetState > initialState) { // New number enters from top slideInVertically { height -> -height } + fadeIn() togetherWith slideOutVertically { height -> height } + fadeOut() } else { // New number enters from bottom slideInVertically { height -> height } + fadeIn() togetherWith slideOutVertically { height -> -height } + fadeOut() } direction.using(SizeTransform(clip = false)) }, label = "counter" ) { targetCount -> Text( text = "$targetCount", style = MaterialTheme.typography.displayLarge, fontWeight = FontWeight.Bold ) } } ``` > **Hiệu năng với AnimatedContent** > > Nội dung bên trong `AnimatedContent` được tái cấu thành mỗi khi `targetState` thay đổi. Đối với nội dung phức tạp, nên ghi nhớ các phần tử tốn kém hoặc sử dụng chiến lược khóa phù hợp. ## Hoạt ảnh vô hạn với rememberInfiniteTransition Đối với hoạt ảnh lặp (chỉ báo tải, hiệu ứng nhịp đập), `rememberInfiniteTransition` cung cấp một API chuyên dụng không yêu cầu quản lý chu kỳ thủ công. ```kotlin // InfiniteTransitionExample.kt @Composable fun PulsingDot( color: Color = MaterialTheme.colorScheme.primary, modifier: Modifier = Modifier ) { val infiniteTransition = rememberInfiniteTransition(label = "pulse") // Looping scale animation val scale by infiniteTransition.animateFloat( initialValue = 0.8f, targetValue = 1.2f, animationSpec = infiniteRepeatable( animation = tween(600, easing = FastOutSlowInEasing), repeatMode = RepeatMode.Reverse ), label = "scale" ) // Synchronized opacity animation val alpha by infiniteTransition.animateFloat( initialValue = 0.5f, targetValue = 1f, animationSpec = infiniteRepeatable( animation = tween(600, easing = FastOutSlowInEasing), repeatMode = RepeatMode.Reverse ), label = "alpha" ) Box( modifier = modifier .size(24.dp) .scale(scale) .alpha(alpha) .background(color = color, shape = CircleShape) ) } ``` ## Hoạt ảnh danh sách với LazyColumn Hoạt ảnh cho các phần tử danh sách đòi hỏi sự chú ý đặc biệt. Modifier `animateItem()` (trước đây là `animateItemPlacement`) tự động tạo hoạt ảnh cho việc sắp xếp lại. ```kotlin // ListAnimationExample.kt @Composable fun AnimatedTaskList( tasks: List, onToggle: (Task) -> Unit, onDelete: (Task) -> Unit, modifier: Modifier = Modifier ) { LazyColumn( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp) ) { items( items = tasks, key = { it.id } // Stable key required for animateItem ) { task -> var isVisible by remember { mutableStateOf(true) } // Exit animation before deletion AnimatedVisibility( visible = isVisible, exit = shrinkVertically() + fadeOut() ) { TaskItem( task = task, onToggle = { onToggle(task) }, onDelete = { isVisible = false // Delay to let animation complete }, modifier = Modifier.animateItem( fadeInSpec = tween(300), fadeOutSpec = tween(300), placementSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ) ) ) } // Trigger deletion after animation LaunchedEffect(isVisible) { if (!isVisible) { delay(300) onDelete(task) } } } } } ``` > **Khóa ổn định cho hoạt ảnh danh sách** > > Không có tham số `key` ổn định, `animateItem` không thể theo dõi các phần tử giữa các lần tái cấu thành. Nên dùng ID duy nhất thay vì chỉ số danh sách. ## Hoạt ảnh với Canvas Đối với các hiệu ứng hình ảnh tùy chỉnh, kết hợp `Canvas` với các giá trị hoạt ảnh mang lại sự linh hoạt hoàn toàn. ```kotlin // CanvasAnimationExample.kt @Composable fun AnimatedProgressRing( progress: Float, // 0f to 1f modifier: Modifier = Modifier ) { // Progress animation with spring for natural feel val animatedProgress by animateFloatAsState( targetValue = progress, animationSpec = spring( dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessVeryLow ), label = "progress" ) // Continuous rotation animation val infiniteTransition = rememberInfiniteTransition(label = "rotation") val rotation by infiniteTransition.animateFloat( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable( animation = tween(2000, easing = LinearEasing) ), label = "rotation" ) val primaryColor = MaterialTheme.colorScheme.primary val trackColor = MaterialTheme.colorScheme.surfaceVariant Canvas( modifier = modifier .size(120.dp) .rotate(rotation) ) { val strokeWidth = 12.dp.toPx() val radius = (size.minDimension - strokeWidth) / 2 // Background circle (track) drawCircle( color = trackColor, radius = radius, style = Stroke(width = strokeWidth, cap = StrokeCap.Round) ) // Animated progress arc drawArc( color = primaryColor, startAngle = -90f, sweepAngle = animatedProgress * 360f, useCenter = false, style = Stroke(width = strokeWidth, cap = StrokeCap.Round), topLeft = Offset(strokeWidth / 2, strokeWidth / 2), size = Size(radius * 2, radius * 2) ) } } ``` ## Tối ưu hóa hiệu năng hoạt ảnh Hoạt ảnh không được tối ưu có thể gây jank và làm hao pin. Đây là những thực hành tốt nhất để duy trì 60 FPS. Quy tắc đầu tiên là tránh cấp phát trong khi hoạt ảnh đang chạy. Nên dùng `graphicsLayer` thay vì các modifier kích hoạt tái cấu thành. ```kotlin // PerformanceOptimization.kt @Composable fun OptimizedAnimatedCard( isExpanded: Boolean, modifier: Modifier = Modifier ) { val scale by animateFloatAsState( targetValue = if (isExpanded) 1.1f else 1f, label = "scale" ) val alpha by animateFloatAsState( targetValue = if (isExpanded) 1f else 0.8f, label = "alpha" ) Card( modifier = modifier // ✅ graphicsLayer: GPU modifications without recomposition .graphicsLayer { scaleX = scale scaleY = scale this.alpha = alpha } // ❌ Avoid: .scale(scale).alpha(alpha) // These modifiers trigger recompositions ) { Text("Card content") } } // Example with remembered lambda to avoid allocations @Composable fun OptimizedClickableItem( onClick: () -> Unit, content: @Composable () -> Unit ) { // ✅ Stable remembered lambda val interactionSource = remember { MutableInteractionSource() } Box( modifier = Modifier .clickable( interactionSource = interactionSource, indication = ripple(), onClick = onClick ) ) { content() } } ``` Điểm quan trọng thứ hai liên quan đến hoạt ảnh trong danh sách. Cần giới hạn số lượng hoạt ảnh đồng thời và sử dụng `derivedStateOf` cho các phép tính dẫn xuất. ```kotlin // ListPerformance.kt @Composable fun PerformantAnimatedList( items: List, modifier: Modifier = Modifier ) { // Calculate once whether the list is empty val isEmpty by remember { derivedStateOf { items.isEmpty() } } LazyColumn(modifier = modifier) { items( items = items, key = { it.id } ) { item -> // Lightweight animation only on initial appearance var hasAppeared by remember { mutableStateOf(false) } LaunchedEffect(Unit) { hasAppeared = true } val alpha by animateFloatAsState( targetValue = if (hasAppeared) 1f else 0f, animationSpec = tween(200), label = "itemAlpha" ) ItemCard( item = item, modifier = Modifier.graphicsLayer { this.alpha = alpha } ) } } } ``` ## Kết luận Hoạt ảnh trong Jetpack Compose mang đến sự cân bằng giữa dễ sử dụng và kiểm soát nâng cao. Đây là những điểm chính cần ghi nhớ: - ✅ Lựa chọn cấp độ API phù hợp với độ phức tạp (AnimatedVisibility → animate*AsState → Animatable) - ✅ Sử dụng `updateTransition` để điều phối nhiều hoạt ảnh liên quan - ✅ Ưu tiên `spring` cho hoạt ảnh tự nhiên và `tween` cho thời lượng chính xác - ✅ Luôn cung cấp tham số `key` ổn định cho hoạt ảnh danh sách - ✅ Tối ưu bằng `graphicsLayer` để tránh tái cấu thành không cần thiết - ✅ Kiểm tra hoạt ảnh trên thiết bị thật để xác nhận hiệu năng Thành thạo hoạt ảnh Compose tạo nên sự khác biệt cho các ứng dụng Android chuyên nghiệp. Những kỹ thuật này, kết hợp với sự chú ý đến hiệu năng, cho phép tạo ra trải nghiệm người dùng mượt mà và hấp dẫn. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/android/jetpack-compose-advanced-animations