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 lanjutan Jetpack Compose untuk pengembang Android

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

AnimationLevels.ktkotlin
// 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 +.

AnimatedVisibilityExample.ktkotlin
@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.

AnimateAsStateExample.ktkotlin
@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.

TransitionExample.ktkotlin
// 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.

AnimatableExample.ktkotlin
@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.

AnimatedContentExample.ktkotlin
@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.

InfiniteTransitionExample.ktkotlin
@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.

ListAnimationExample.ktkotlin
@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 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.

CanvasAnimationExample.ktkotlin
@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.

PerformanceOptimization.ktkotlin
@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.

ListPerformance.ktkotlin
@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 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.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

#jetpack compose
#android
#animations
#kotlin
#ui

Bagikan

Artikel terkait