# Jetpack Compose: Animasi Lanjutan Langkah demi Langkah > Panduan lengkap animasi lanjutan di Compose: transisi, AnimatedVisibility, Animatable, gestur, dan performa untuk antarmuka Android yang halus. - Published: 2026-02-08 - Updated: 2026-04-28 - Author: SharpSkill - Tags: jetpack compose, android, animations, kotlin, ui - Reading time: 12 min --- Animasi mengubah aplikasi yang fungsional menjadi pengalaman pengguna yang berkesan. Jetpack Compose menyediakan API animasi deklaratif yang kuat sehingga sangat menyederhanakan pembuatan antarmuka yang halus. Panduan ini membahas teknik lanjutan untuk membangun animasi yang berperforma dan mudah dipelihara. > **Prasyarat** > > Tutorial ini mengasumsikan keakraban dengan dasar-dasar Compose (recomposition, state, modifier). Untuk dasarnya, sebaiknya membaca terlebih dahulu panduan pertanyaan wawancara Jetpack Compose. ## Dasar-dasar API Animasi di Compose Compose menyediakan beberapa tingkat API untuk animasi. Pilihannya bergantung pada tingkat kontrol yang diinginkan dan kompleksitas animasi. API ini terbagi menjadi tiga kategori utama: animasi tingkat tinggi (`AnimatedVisibility`, `AnimatedContent`), animasi berbasis state (`animate*AsState`), dan animasi tingkat rendah (`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) } } ``` Memilih tingkat API yang tepat sangat penting untuk menjaga kode tetap mudah dibaca sambil mempertahankan fleksibilitas yang dibutuhkan. ## AnimatedVisibility: Animasi Masuk dan Keluar yang Elegan `AnimatedVisibility` adalah titik awal yang ideal untuk menganimasikan kemunculan dan hilangnya elemen. API ini secara otomatis mengelola komposisi dan dekomposisi konten. Parameter `enter` dan `exit` menerima kombinasi transisi yang menentukan perilaku animasi. Transisi ini dapat digabungkan dengan operator `+`. ```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 ) } } } } ``` Konten di dalam `AnimatedVisibility` hanya disusun ketika `visible = true`, sehingga performa pada daftar dengan banyak elemen yang dapat diperluas menjadi optimal. > **Menggabungkan transisi** > > Transisi yang tersedia mencakup `fadeIn/fadeOut`, `slideIn/slideOut`, `expandIn/shrinkOut`, `scaleIn/scaleOut`. Transisi tersebut dapat digabungkan secara bebas untuk menciptakan efek kustom. ## animate*AsState: Animasi yang Dipicu State Keluarga fungsi `animate*AsState` menganimasikan perubahan nilai primitif secara otomatis. Ini adalah pendekatan paling idiomatik untuk animasi sederhana di Compose. Setiap tipe data memiliki fungsi tersendiri: `animateColorAsState`, `animateFloatAsState`, `animateDpAsState`, `animateIntAsState`, dan seterusnya. ```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 ) } } ``` Parameter `animationSpec` mengontrol perilaku temporal animasi. Dua spec yang paling umum digunakan adalah `tween` (durasi tetap dengan easing) dan `spring` (fisika realistis dengan pantulan). ## Transition: Mengoordinasikan Beberapa Animasi Ketika beberapa properti perlu dianimasikan secara terkoordinasi, `updateTransition` menyediakan kontrol terpusat. API ini memastikan semua animasi tetap sinkron. Polanya adalah mendefinisikan state berbentuk enum, lalu membuat animasi untuk setiap properti yang bergantung pada state tersebut. ```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: Kontrol Penuh atas Animasi `Animatable` adalah API tingkat rendah yang menyediakan kontrol programatis penuh. Pendekatan ini diperlukan untuk animasi yang dapat diinterupsi, gestur, atau skenario kompleks. Berbeda dengan `animate*AsState`, `Animatable` memungkinkan untuk menghentikan, membalik, atau memodifikasi animasi yang sedang berjalan tanpa menunggu sampai selesai. ```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() } } ``` Metode utama `Animatable` adalah `animateTo()` (menganimasikan menuju target), `snapTo()` (perubahan instan), dan `stop()` (interupsi). ## AnimatedContent: Transisi Konten `AnimatedContent` menganimasikan transisi antara konten yang berbeda. API ini sempurna untuk perubahan state yang sepenuhnya mengubah UI yang ditampilkan. Kunci `targetState` menentukan kapan transisi harus terjadi. `transitionSpec` mendefinisikan bagaimana konten yang keluar dan masuk berinteraksi. ```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 ) } } ``` > **Performa dengan AnimatedContent** > > Konten di dalam `AnimatedContent` direkomposisi pada setiap perubahan `targetState`. Untuk konten yang kompleks, sebaiknya melakukan memoisasi pada elemen yang mahal atau menggunakan strategi key yang sesuai. ## Animasi Tak Terbatas dengan rememberInfiniteTransition Untuk animasi berulang (indikator pemuatan, efek denyut), `rememberInfiniteTransition` menyediakan API khusus yang tidak memerlukan pengelolaan siklus secara manual. ```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) ) } ``` ## Animasi Daftar dengan LazyColumn Animasi item daftar memerlukan perhatian khusus. Modifier `animateItem()` (sebelumnya `animateItemPlacement`) secara otomatis menganimasikan penyusunan ulang. ```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 yang stabil untuk animasi daftar** > > Tanpa parameter `key` yang stabil, `animateItem` tidak dapat melacak item antar rekomposisi. Sebaiknya menggunakan ID unik daripada indeks daftar. ## Animasi dengan Canvas Untuk efek visual kustom, menggabungkan `Canvas` dengan nilai animasi memberikan fleksibilitas total. ```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) ) } } ``` ## Optimasi Performa Animasi Animasi yang tidak dioptimalkan dengan baik dapat menyebabkan jank dan menguras baterai. Berikut praktik terbaik untuk mempertahankan 60 FPS. Aturan pertama adalah menghindari alokasi selama animasi. Sebaiknya menggunakan `graphicsLayer` daripada modifier yang memicu rekomposisi. ```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() } } ``` Poin kritis kedua menyangkut animasi pada daftar. Jumlah animasi simultan perlu dibatasi dan `derivedStateOf` digunakan untuk perhitungan turunan. ```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 } ) } } } ``` ## Kesimpulan Animasi di Jetpack Compose menawarkan keseimbangan antara kemudahan penggunaan dan kontrol lanjutan. Berikut poin-poin kunci yang perlu diingat: - ✅ Memilih tingkat API yang sesuai dengan kompleksitas (AnimatedVisibility → animate*AsState → Animatable) - ✅ Menggunakan `updateTransition` untuk mengoordinasikan beberapa animasi yang terkait - ✅ Memilih `spring` untuk animasi yang alami dan `tween` untuk durasi yang presisi - ✅ Selalu menyediakan parameter `key` yang stabil untuk animasi daftar - ✅ Mengoptimalkan dengan `graphicsLayer` untuk menghindari rekomposisi yang tidak perlu - ✅ Menguji animasi pada perangkat nyata untuk memvalidasi performa Menguasai animasi Compose membedakan aplikasi Android profesional. Teknik-teknik ini, dipadukan dengan perhatian pada performa, memungkinkan terciptanya pengalaman pengguna yang halus dan menarik. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/android/jetpack-compose-advanced-animations