Jetpack Compose: Animasi Lanjutan Langkah demi Langkah
Panduan lengkap animasi lanjutan di Compose: transisi, AnimatedVisibility, Animatable, gestur, dan performa untuk antarmuka Android yang halus.

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.
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).
// 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 +.
@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.
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.
@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.
// 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
}
}Siap menguasai wawancara Android Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
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.
@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.
@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
)
}
}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.
@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.
@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)
}
}
}
}
}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.
@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)
)
}
}Siap menguasai wawancara Android Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
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.
@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.
@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 }
)
}
}
}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
updateTransitionuntuk mengoordinasikan beberapa animasi yang terkait - ✅ Memilih
springuntuk animasi yang alami dantweenuntuk durasi yang presisi - ✅ Selalu menyediakan parameter
keyyang stabil untuk animasi daftar - ✅ Mengoptimalkan dengan
graphicsLayeruntuk 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.
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

20 Pertanyaan Wawancara Jetpack Compose Teratas di Tahun 2026
20 pertanyaan wawancara Jetpack Compose yang paling sering ditanyakan: recomposition, state management, navigation, performa, dan pola arsitektur dengan contoh kode lengkap.

Kotlin 2.3 untuk Android: Name-Based Destructuring, KMP dan Pertanyaan Wawancara 2026
Pertanyaan wawancara Kotlin 2.3 untuk developer Android di tahun 2026. Name-based destructuring, KMP, context parameters, Flow, dan coroutines dengan contoh kode.

MVVM vs MVI di Android: Arsitektur Mana yang Dipilih di 2026?
Perbandingan mendalam antara MVVM dan MVI di Android: kelebihan, keterbatasan, kasus penggunaan, dan panduan praktis memilih arsitektur yang tepat di 2026.