# Jetpack Compose: แอนิเมชันขั้นสูงทีละขั้นตอน > คู่มือฉบับสมบูรณ์เกี่ยวกับแอนิเมชันขั้นสูงใน Compose: ทรานซิชัน AnimatedVisibility, Animatable, ท่าทาง และประสิทธิภาพสำหรับอินเทอร์เฟซ Android ที่ลื่นไหล - Published: 2026-02-08 - Updated: 2026-04-28 - Author: SharpSkill - Tags: jetpack compose, android, animations, kotlin, ui - Reading time: 12 min --- แอนิเมชันเปลี่ยนแอปพลิเคชันเชิงฟังก์ชันให้กลายเป็นประสบการณ์ผู้ใช้ที่น่าจดจำ Jetpack Compose มี API แอนิเมชันแบบประกาศที่ทรงพลังซึ่งช่วยให้การสร้างอินเทอร์เฟซที่ลื่นไหลง่ายขึ้นอย่างมาก คู่มือนี้สำรวจเทคนิคขั้นสูงในการสร้างแอนิเมชันที่มีประสิทธิภาพและบำรุงรักษาง่าย > **ข้อกำหนดเบื้องต้น** > > บทแนะนำนี้สันนิษฐานว่าผู้อ่านคุ้นเคยกับพื้นฐานของ Compose (recomposition, state, modifier) สำหรับพื้นฐานควรอ่านคู่มือคำถามสัมภาษณ์ Jetpack Compose ก่อน ## พื้นฐานของ API แอนิเมชันใน Compose Compose มี API หลายระดับสำหรับแอนิเมชัน การเลือกขึ้นอยู่กับระดับการควบคุมที่ต้องการและความซับซ้อนของแอนิเมชัน API แบ่งออกเป็นสามประเภทหลัก ได้แก่ แอนิเมชันระดับสูง (`AnimatedVisibility`, `AnimatedContent`) แอนิเมชันที่อิงตามสถานะ (`animate*AsState`) และแอนิเมชันระดับต่ำ (`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) } } ``` การเลือกระดับ API ที่เหมาะสมเป็นสิ่งสำคัญเพื่อรักษาโค้ดให้อ่านง่ายขณะที่ยังคงความยืดหยุ่นที่จำเป็น ## AnimatedVisibility: แอนิเมชันการเข้าและออกที่หรูหรา `AnimatedVisibility` เป็นจุดเริ่มต้นที่เหมาะสำหรับการแอนิเมตการปรากฏและหายไปของอิลิเมนต์ API นี้จัดการการคอมโพสและดีคอมโพสเนื้อหาโดยอัตโนมัติ พารามิเตอร์ `enter` และ `exit` รับการรวมกันของทรานซิชันที่กำหนดพฤติกรรมของแอนิเมชัน ทรานซิชันเหล่านี้สามารถรวมกันด้วยตัวดำเนินการ `+` ```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 ) } } } } ``` เนื้อหาภายใน `AnimatedVisibility` จะถูกคอมโพสเฉพาะเมื่อ `visible = true` ซึ่งช่วยปรับประสิทธิภาพสำหรับรายการที่มีอิลิเมนต์ที่ขยายได้จำนวนมาก > **การรวมทรานซิชัน** > > ทรานซิชันที่ใช้ได้ ได้แก่ `fadeIn/fadeOut`, `slideIn/slideOut`, `expandIn/shrinkOut`, `scaleIn/scaleOut` สามารถรวมกันได้อย่างอิสระเพื่อสร้างเอฟเฟกต์ที่กำหนดเอง ## animate*AsState: แอนิเมชันที่ขับเคลื่อนด้วยสถานะ กลุ่มฟังก์ชัน `animate*AsState` แอนิเมตการเปลี่ยนแปลงของค่าพื้นฐานโดยอัตโนมัติ นี่คือแนวทางที่เป็นเอกลักษณ์ที่สุดสำหรับแอนิเมชันธรรมดาใน Compose ข้อมูลแต่ละประเภทมีฟังก์ชันเฉพาะของตน: `animateColorAsState`, `animateFloatAsState`, `animateDpAsState`, `animateIntAsState` เป็นต้น ```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 ) } } ``` พารามิเตอร์ `animationSpec` ควบคุมพฤติกรรมเชิงเวลาของแอนิเมชัน Spec ที่ใช้บ่อยที่สุดสองรายการคือ `tween` (ระยะเวลาคงที่พร้อม easing) และ `spring` (ฟิสิกส์ที่สมจริงพร้อมการเด้ง) ## Transition: การประสานงานหลายแอนิเมชัน เมื่อต้องแอนิเมตคุณสมบัติหลายอย่างให้ประสานกัน `updateTransition` ให้การควบคุมแบบรวมศูนย์ API นี้รับประกันว่าแอนิเมชันทั้งหมดจะคงการประสานเวลาไว้ รูปแบบนี้ประกอบด้วยการกำหนดสถานะแบบ enum จากนั้นสร้างแอนิเมชันสำหรับคุณสมบัติแต่ละอย่างที่ขึ้นอยู่กับสถานะนั้น ```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: การควบคุมแอนิเมชันเต็มรูปแบบ `Animatable` เป็น API ระดับต่ำที่ให้การควบคุมเชิงโปรแกรมอย่างสมบูรณ์ แนวทางนี้จำเป็นสำหรับแอนิเมชันที่สามารถถูกขัดจังหวะได้ ท่าทาง หรือสถานการณ์ที่ซับซ้อน ต่างจาก `animate*AsState`, `Animatable` ช่วยให้สามารถหยุด ย้อนกลับ หรือปรับเปลี่ยนแอนิเมชันที่กำลังดำเนินการได้โดยไม่ต้องรอให้เสร็จสิ้น ```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() } } ``` เมธอดสำคัญของ `Animatable` คือ `animateTo()` (แอนิเมตไปยังเป้าหมาย) `snapTo()` (เปลี่ยนทันที) และ `stop()` (ขัดจังหวะ) ## AnimatedContent: ทรานซิชันของเนื้อหา `AnimatedContent` แอนิเมตทรานซิชันระหว่างเนื้อหาที่แตกต่างกัน API นี้เหมาะสำหรับการเปลี่ยนแปลงสถานะที่ปรับเปลี่ยน UI ที่แสดงโดยสิ้นเชิง คีย์ `targetState` กำหนดว่าทรานซิชันควรเกิดขึ้นเมื่อใด `transitionSpec` กำหนดว่าเนื้อหาที่ออกและเนื้อหาที่เข้าจะมีปฏิสัมพันธ์กันอย่างไร ```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 ) } } ``` > **ประสิทธิภาพของ AnimatedContent** > > เนื้อหาภายใน `AnimatedContent` จะถูก recompose ทุกครั้งที่ `targetState` เปลี่ยน สำหรับเนื้อหาที่ซับซ้อน ควรเก็บอิลิเมนต์ที่มีต้นทุนสูงไว้ในแคชหรือใช้กลยุทธ์คีย์ที่เหมาะสม ## แอนิเมชันไม่จำกัดด้วย rememberInfiniteTransition สำหรับแอนิเมชันที่วนซ้ำ (ตัวบ่งชี้การโหลด เอฟเฟกต์การเต้น) `rememberInfiniteTransition` มี API เฉพาะที่ไม่ต้องจัดการรอบด้วยตนเอง ```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) ) } ``` ## แอนิเมชันรายการด้วย LazyColumn แอนิเมชันสำหรับรายการต้องให้ความสำคัญเป็นพิเศษ Modifier `animateItem()` (เดิมคือ `animateItemPlacement`) แอนิเมตการจัดเรียงใหม่โดยอัตโนมัติ ```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) } } } } } ``` > **คีย์ที่เสถียรสำหรับแอนิเมชันรายการ** > > หากไม่มีพารามิเตอร์ `key` ที่เสถียร `animateItem` จะไม่สามารถติดตามอิลิเมนต์ระหว่างการ recompose ได้ ควรใช้ ID ที่ไม่ซ้ำแทนดัชนีของรายการ ## แอนิเมชัน Canvas สำหรับเอฟเฟกต์ภาพที่กำหนดเอง การรวม `Canvas` กับค่าที่แอนิเมตให้ความยืดหยุ่นทั้งหมด ```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) ) } } ``` ## การเพิ่มประสิทธิภาพแอนิเมชัน แอนิเมชันที่ปรับแต่งไม่ดีอาจทำให้เกิดอาการกระตุก (jank) และทำให้แบตเตอรี่หมดเร็ว นี่คือแนวทางปฏิบัติที่ดีที่สุดในการรักษา 60 FPS กฎข้อแรกคือหลีกเลี่ยงการจัดสรรหน่วยความจำระหว่างแอนิเมชัน ควรใช้ `graphicsLayer` แทน modifier ที่กระตุ้นการ recompose ```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() } } ``` จุดสำคัญลำดับที่สองเกี่ยวข้องกับแอนิเมชันในรายการ ควรจำกัดจำนวนแอนิเมชันที่เกิดขึ้นพร้อมกันและใช้ `derivedStateOf` สำหรับการคำนวณที่อนุมานได้ ```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 } ) } } } ``` ## บทสรุป แอนิเมชันใน Jetpack Compose มอบความสมดุลระหว่างความง่ายในการใช้งานและการควบคุมขั้นสูง นี่คือประเด็นสำคัญที่ควรจดจำ: - ✅ เลือกระดับ API ที่เหมาะสมตามความซับซ้อน (AnimatedVisibility → animate*AsState → Animatable) - ✅ ใช้ `updateTransition` เพื่อประสานหลายแอนิเมชันที่เกี่ยวข้องกัน - ✅ เลือก `spring` สำหรับแอนิเมชันที่เป็นธรรมชาติ และ `tween` สำหรับระยะเวลาที่แม่นยำ - ✅ ระบุพารามิเตอร์ `key` ที่เสถียรเสมอสำหรับแอนิเมชันรายการ - ✅ ปรับแต่งด้วย `graphicsLayer` เพื่อหลีกเลี่ยงการ recompose ที่ไม่จำเป็น - ✅ ทดสอบแอนิเมชันบนอุปกรณ์จริงเพื่อยืนยันประสิทธิภาพ การเชี่ยวชาญแอนิเมชัน Compose ทำให้แอป Android ระดับมืออาชีพแตกต่างจากที่อื่น เทคนิคเหล่านี้ผสมผสานกับการให้ความสำคัญกับประสิทธิภาพ ทำให้สามารถสร้างประสบการณ์ผู้ใช้ที่ลื่นไหลและน่าสนใจ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/android/jetpack-compose-advanced-animations