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à.

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ì.
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).
// 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ử +.
@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.
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.
@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 đó.
// 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
}
}Sẵn sàng chinh phục phỏng vấn Android?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
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.
@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.
@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
)
}
}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.
@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.
@Composable
fun AnimatedTaskList(
tasks: List<Task>,
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ô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.
@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)
)
}
}Sẵn sàng chinh phục phỏng vấn Android?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
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.
@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.
@Composable
fun PerformantAnimatedList(
items: List<Item>,
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
springcho hoạt ảnh tự nhiên vàtweencho 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.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

20 Câu Hỏi Phỏng Vấn Jetpack Compose Hàng Đầu Năm 2026
20 câu hỏi phỏng vấn Jetpack Compose thường gặp nhất: recomposition, quản lý state, navigation, hiệu năng và các pattern kiến trúc với ví dụ code chi tiết.

Kotlin 2.3 cho Android: Name-Based Destructuring, KMP và Câu Hỏi Phỏng Vấn 2026
Câu hỏi phỏng vấn Kotlin 2.3 dành cho lập trình viên Android năm 2026. Name-based destructuring, KMP, context parameters, Flow và coroutines kèm ví dụ code.

MVVM vs MVI: Kiến Trúc Nào Phù Hợp Cho Android Năm 2026?
So sánh chuyên sâu MVVM và MVI trên Android: ưu điểm, nhược điểm, trường hợp sử dụng và hướng dẫn thực tiễn để lựa chọn kiến trúc phù hợp trong năm 2026.