Jetpack Compose: แอนิเมชันขั้นสูงทีละขั้นตอน
คู่มือฉบับสมบูรณ์เกี่ยวกับแอนิเมชันขั้นสูงใน Compose: ทรานซิชัน AnimatedVisibility, Animatable, ท่าทาง และประสิทธิภาพสำหรับอินเทอร์เฟซ Android ที่ลื่นไหล

แอนิเมชันเปลี่ยนแอปพลิเคชันเชิงฟังก์ชันให้กลายเป็นประสบการณ์ผู้ใช้ที่น่าจดจำ Jetpack Compose มี API แอนิเมชันแบบประกาศที่ทรงพลังซึ่งช่วยให้การสร้างอินเทอร์เฟซที่ลื่นไหลง่ายขึ้นอย่างมาก คู่มือนี้สำรวจเทคนิคขั้นสูงในการสร้างแอนิเมชันที่มีประสิทธิภาพและบำรุงรักษาง่าย
บทแนะนำนี้สันนิษฐานว่าผู้อ่านคุ้นเคยกับพื้นฐานของ Compose (recomposition, state, modifier) สำหรับพื้นฐานควรอ่านคู่มือคำถามสัมภาษณ์ Jetpack Compose ก่อน
พื้นฐานของ API แอนิเมชันใน Compose
Compose มี API หลายระดับสำหรับแอนิเมชัน การเลือกขึ้นอยู่กับระดับการควบคุมที่ต้องการและความซับซ้อนของแอนิเมชัน
API แบ่งออกเป็นสามประเภทหลัก ได้แก่ แอนิเมชันระดับสูง (AnimatedVisibility, AnimatedContent) แอนิเมชันที่อิงตามสถานะ (animate*AsState) และแอนิเมชันระดับต่ำ (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)
}
}การเลือกระดับ API ที่เหมาะสมเป็นสิ่งสำคัญเพื่อรักษาโค้ดให้อ่านง่ายขณะที่ยังคงความยืดหยุ่นที่จำเป็น
AnimatedVisibility: แอนิเมชันการเข้าและออกที่หรูหรา
AnimatedVisibility เป็นจุดเริ่มต้นที่เหมาะสำหรับการแอนิเมตการปรากฏและหายไปของอิลิเมนต์ API นี้จัดการการคอมโพสและดีคอมโพสเนื้อหาโดยอัตโนมัติ
พารามิเตอร์ enter และ exit รับการรวมกันของทรานซิชันที่กำหนดพฤติกรรมของแอนิเมชัน ทรานซิชันเหล่านี้สามารถรวมกันด้วยตัวดำเนินการ +
@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
)
}
}
}
}เนื้อหาภายใน AnimatedVisibility จะถูกคอมโพสเฉพาะเมื่อ visible = true ซึ่งช่วยปรับประสิทธิภาพสำหรับรายการที่มีอิลิเมนต์ที่ขยายได้จำนวนมาก
ทรานซิชันที่ใช้ได้ ได้แก่ fadeIn/fadeOut, slideIn/slideOut, expandIn/shrinkOut, scaleIn/scaleOut สามารถรวมกันได้อย่างอิสระเพื่อสร้างเอฟเฟกต์ที่กำหนดเอง
animate*AsState: แอนิเมชันที่ขับเคลื่อนด้วยสถานะ
กลุ่มฟังก์ชัน animate*AsState แอนิเมตการเปลี่ยนแปลงของค่าพื้นฐานโดยอัตโนมัติ นี่คือแนวทางที่เป็นเอกลักษณ์ที่สุดสำหรับแอนิเมชันธรรมดาใน Compose
ข้อมูลแต่ละประเภทมีฟังก์ชันเฉพาะของตน: animateColorAsState, animateFloatAsState, animateDpAsState, animateIntAsState เป็นต้น
@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
)
}
}พารามิเตอร์ animationSpec ควบคุมพฤติกรรมเชิงเวลาของแอนิเมชัน Spec ที่ใช้บ่อยที่สุดสองรายการคือ tween (ระยะเวลาคงที่พร้อม easing) และ spring (ฟิสิกส์ที่สมจริงพร้อมการเด้ง)
Transition: การประสานงานหลายแอนิเมชัน
เมื่อต้องแอนิเมตคุณสมบัติหลายอย่างให้ประสานกัน updateTransition ให้การควบคุมแบบรวมศูนย์ API นี้รับประกันว่าแอนิเมชันทั้งหมดจะคงการประสานเวลาไว้
รูปแบบนี้ประกอบด้วยการกำหนดสถานะแบบ enum จากนั้นสร้างแอนิเมชันสำหรับคุณสมบัติแต่ละอย่างที่ขึ้นอยู่กับสถานะนั้น
// 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
}
}พร้อมที่จะพิชิตการสัมภาษณ์ Android แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Animatable: การควบคุมแอนิเมชันเต็มรูปแบบ
Animatable เป็น API ระดับต่ำที่ให้การควบคุมเชิงโปรแกรมอย่างสมบูรณ์ แนวทางนี้จำเป็นสำหรับแอนิเมชันที่สามารถถูกขัดจังหวะได้ ท่าทาง หรือสถานการณ์ที่ซับซ้อน
ต่างจาก animate*AsState, Animatable ช่วยให้สามารถหยุด ย้อนกลับ หรือปรับเปลี่ยนแอนิเมชันที่กำลังดำเนินการได้โดยไม่ต้องรอให้เสร็จสิ้น
@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()
}
}เมธอดสำคัญของ Animatable คือ animateTo() (แอนิเมตไปยังเป้าหมาย) snapTo() (เปลี่ยนทันที) และ stop() (ขัดจังหวะ)
AnimatedContent: ทรานซิชันของเนื้อหา
AnimatedContent แอนิเมตทรานซิชันระหว่างเนื้อหาที่แตกต่างกัน API นี้เหมาะสำหรับการเปลี่ยนแปลงสถานะที่ปรับเปลี่ยน UI ที่แสดงโดยสิ้นเชิง
คีย์ targetState กำหนดว่าทรานซิชันควรเกิดขึ้นเมื่อใด transitionSpec กำหนดว่าเนื้อหาที่ออกและเนื้อหาที่เข้าจะมีปฏิสัมพันธ์กันอย่างไร
@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
)
}
}เนื้อหาภายใน AnimatedContent จะถูก recompose ทุกครั้งที่ targetState เปลี่ยน สำหรับเนื้อหาที่ซับซ้อน ควรเก็บอิลิเมนต์ที่มีต้นทุนสูงไว้ในแคชหรือใช้กลยุทธ์คีย์ที่เหมาะสม
แอนิเมชันไม่จำกัดด้วย rememberInfiniteTransition
สำหรับแอนิเมชันที่วนซ้ำ (ตัวบ่งชี้การโหลด เอฟเฟกต์การเต้น) rememberInfiniteTransition มี API เฉพาะที่ไม่ต้องจัดการรอบด้วยตนเอง
@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)
)
}แอนิเมชันรายการด้วย LazyColumn
แอนิเมชันสำหรับรายการต้องให้ความสำคัญเป็นพิเศษ Modifier animateItem() (เดิมคือ animateItemPlacement) แอนิเมตการจัดเรียงใหม่โดยอัตโนมัติ
@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)
}
}
}
}
}หากไม่มีพารามิเตอร์ key ที่เสถียร animateItem จะไม่สามารถติดตามอิลิเมนต์ระหว่างการ recompose ได้ ควรใช้ ID ที่ไม่ซ้ำแทนดัชนีของรายการ
แอนิเมชัน Canvas
สำหรับเอฟเฟกต์ภาพที่กำหนดเอง การรวม Canvas กับค่าที่แอนิเมตให้ความยืดหยุ่นทั้งหมด
@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)
)
}
}พร้อมที่จะพิชิตการสัมภาษณ์ Android แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
การเพิ่มประสิทธิภาพแอนิเมชัน
แอนิเมชันที่ปรับแต่งไม่ดีอาจทำให้เกิดอาการกระตุก (jank) และทำให้แบตเตอรี่หมดเร็ว นี่คือแนวทางปฏิบัติที่ดีที่สุดในการรักษา 60 FPS
กฎข้อแรกคือหลีกเลี่ยงการจัดสรรหน่วยความจำระหว่างแอนิเมชัน ควรใช้ graphicsLayer แทน modifier ที่กระตุ้นการ recompose
@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()
}
}จุดสำคัญลำดับที่สองเกี่ยวข้องกับแอนิเมชันในรายการ ควรจำกัดจำนวนแอนิเมชันที่เกิดขึ้นพร้อมกันและใช้ derivedStateOf สำหรับการคำนวณที่อนุมานได้
@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 }
)
}
}
}บทสรุป
แอนิเมชันใน Jetpack Compose มอบความสมดุลระหว่างความง่ายในการใช้งานและการควบคุมขั้นสูง นี่คือประเด็นสำคัญที่ควรจดจำ:
- ✅ เลือกระดับ API ที่เหมาะสมตามความซับซ้อน (AnimatedVisibility → animate*AsState → Animatable)
- ✅ ใช้
updateTransitionเพื่อประสานหลายแอนิเมชันที่เกี่ยวข้องกัน - ✅ เลือก
springสำหรับแอนิเมชันที่เป็นธรรมชาติ และtweenสำหรับระยะเวลาที่แม่นยำ - ✅ ระบุพารามิเตอร์
keyที่เสถียรเสมอสำหรับแอนิเมชันรายการ - ✅ ปรับแต่งด้วย
graphicsLayerเพื่อหลีกเลี่ยงการ recompose ที่ไม่จำเป็น - ✅ ทดสอบแอนิเมชันบนอุปกรณ์จริงเพื่อยืนยันประสิทธิภาพ
การเชี่ยวชาญแอนิเมชัน Compose ทำให้แอป Android ระดับมืออาชีพแตกต่างจากที่อื่น เทคนิคเหล่านี้ผสมผสานกับการให้ความสำคัญกับประสิทธิภาพ ทำให้สามารถสร้างประสบการณ์ผู้ใช้ที่ลื่นไหลและน่าสนใจ
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

20 คำถามสัมภาษณ์ Jetpack Compose ยอดนิยมประจำปี 2026
20 คำถามสัมภาษณ์ Jetpack Compose ที่พบบ่อยที่สุด: recomposition, การจัดการ state, navigation, ประสิทธิภาพ และ pattern สถาปัตยกรรม พร้อมตัวอย่างโค้ดละเอียด

Kotlin Flow vs StateFlow vs SharedFlow: คำถามสัมภาษณ์ Android ปี 2026
คำถาม Kotlin Flow vs StateFlow vs SharedFlow ที่ผู้สัมภาษณ์ Android ถามในปี 2026 พร้อมคำตอบชัดเจน ตารางเปรียบเทียบ และโค้ดที่พร้อมใช้งานจริง

Android 16 ในปี 2026: API ใหม่ Desktop Mode และคำถามสัมภาษณ์
เจาะลึก Android 16 (API 36) ครอบคลุม desktop mode, การแจ้งเตือน ProgressStyle, predictive back, การบังคับใช้ edge-to-edge และคำถามสัมภาษณ์ที่มาพร้อมกับการเปลี่ยนแปลงเหล่านี้