Panduan cepat untuk Animasi di Compose

Compose memiliki banyak mekanisme animasi bawaan dan mungkin sulit untuk mengetahui mana yang harus dipilih. Berikut adalah daftar kasus penggunaan animasi yang umum. Untuk mengetahui informasi yang lebih mendetail tentang serangkaian lengkap opsi API yang berbeda yang tersedia untuk Anda, baca dokumentasi Animasi Compose selengkapnya.

Menganimasikan properti composable umum

Compose menyediakan API yang praktis yang memungkinkan Anda mengatasi banyak kasus penggunaan animasi umum. Bagian ini menunjukkan cara menganimasikan properti umum composable.

Menganimasikan kemunculan / penghilangan

Composable hijau yang menampilkan dan menyembunyikan dirinya sendiri
Gambar 1. Menganimasikan kemunculan dan hilangnya item di Kolom

Gunakan AnimatedVisibility untuk menyembunyikan atau menampilkan Composable. Turunan di dalam AnimatedVisibility dapat menggunakan Modifier.animateEnterExit() untuk transisi masuk atau keluarnya sendiri.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

Parameter masuk dan keluar AnimatedVisibility memungkinkan Anda mengonfigurasi perilaku composable saat muncul dan menghilang. Baca dokumentasi lengkap untuk mengetahui informasi selengkapnya.

Opsi lain untuk menganimasikan visibilitas composable adalah menganimasikan alpha dari waktu ke waktu menggunakan animateFloatAsState:

var visible by remember {
    mutableStateOf(true)
}
val animatedAlpha by animateFloatAsState(
    targetValue = if (visible) 1.0f else 0f,
    label = "alpha"
)
Box(
    modifier = Modifier
        .size(200.dp)
        .graphicsLayer {
            alpha = animatedAlpha
        }
        .clip(RoundedCornerShape(8.dp))
        .background(colorGreen)
        .align(Alignment.TopCenter)
) {
}

Namun, mengubah alfa disertai dengan peringatan bahwa composable tetap dalam komposisi dan terus menempati ruang tempatnya disusun. Hal ini dapat menyebabkan pembaca layar dan mekanisme aksesibilitas lainnya tetap mempertimbangkan item di layar. Di sisi lain, AnimatedVisibility pada akhirnya akan menghapus item dari komposisi.

Menganimasikan alfa composable
Gambar 2. Menganimasikan alfa composable

Menganimasikan warna latar belakang

Composable dengan warna latar belakang yang berubah dari waktu ke waktu sebagai animasi, dengan warna memudar satu sama lain.
Gambar 3. Menganimasikan warna latar belakang composable

val animatedColor by animateColorAsState(
    if (animateBackgroundColor) colorGreen else colorBlue,
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(animatedColor)
    }
) {
    // your composable here
}

Opsi ini memiliki performa yang lebih baik daripada menggunakan Modifier.background(). Modifier.background() dapat diterima untuk setelan warna satu kali, tetapi saat mengoanimasi warna dari waktu ke waktu, hal ini dapat menyebabkan lebih banyak rekomposisi daripada yang diperlukan.

Untuk menganimasikan warna latar belakang tanpa batas, lihat mengulang bagian animasi.

Menganimasikan ukuran composable

Composable hijau menganimasikan perubahan ukurannya dengan lancar.
Gambar 4. Composable yang menganimasikan dengan lancar antara ukuran kecil dan besar

Compose memungkinkan Anda menganimasikan ukuran composable dengan beberapa cara. Gunakan animateContentSize() untuk animasi di antara perubahan ukuran composable.

Misalnya, jika Anda memiliki kotak yang berisi teks yang dapat diperluas dari satu hingga beberapa baris, Anda dapat menggunakan Modifier.animateContentSize() untuk mendapatkan transisi yang lebih lancar:

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

Anda juga dapat menggunakan AnimatedContent, dengan SizeTransform untuk mendeskripsikan cara perubahan ukuran harus dilakukan.

Menganimasikan posisi composable

Composable hijau yang dianimasikan dengan lancar ke bawah dan ke kanan
Gambar 5. Composable yang dipindahkan berdasarkan offset

Untuk menganimasikan posisi composable, gunakan Modifier.offset{ } yang digabungkan dengan animateIntOffsetAsState().

var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
    100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
    targetValue = if (moved) {
        IntOffset(pxToMove, pxToMove)
    } else {
        IntOffset.Zero
    },
    label = "offset"
)

Box(
    modifier = Modifier
        .offset {
            offset
        }
        .background(colorBlue)
        .size(100.dp)
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            moved = !moved
        }
)

Jika Anda ingin memastikan bahwa composable tidak digambar di atas atau di bawah composable lain saat menganimasikan posisi atau ukuran, gunakan Modifier.layout{ }. Pengubah ini menyebarkan perubahan ukuran dan posisi ke induk, yang kemudian memengaruhi turunan lainnya.

Misalnya, jika Anda memindahkan Box dalam Column dan turunan lainnya perlu dipindahkan saat Box dipindahkan, sertakan informasi offset dengan Modifier.layout{ } sebagai berikut:

var toggled by remember {
    mutableStateOf(false)
}
val interactionSource = remember {
    MutableInteractionSource()
}
Column(
    modifier = Modifier
        .padding(16.dp)
        .fillMaxSize()
        .clickable(indication = null, interactionSource = interactionSource) {
            toggled = !toggled
        }
) {
    val offsetTarget = if (toggled) {
        IntOffset(150, 150)
    } else {
        IntOffset.Zero
    }
    val offset = animateIntOffsetAsState(
        targetValue = offsetTarget, label = "offset"
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
    Box(
        modifier = Modifier
            .layout { measurable, constraints ->
                val offsetValue = if (isLookingAhead) offsetTarget else offset.value
                val placeable = measurable.measure(constraints)
                layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) {
                    placeable.placeRelative(offsetValue)
                }
            }
            .size(100.dp)
            .background(colorGreen)
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
}

2 kotak dengan kotak ke-2 menganimasikan posisi X,Y-nya, kotak ketiga merespons dengan memindahkan dirinya sendiri dengan jumlah Y juga.
Gambar 6. Menganimasikan dengan Modifier.layout{ }

Menganimasikan padding composable

Composable hijau menjadi lebih kecil dan lebih besar saat diklik, dengan padding yang dianimasikan
Gambar 7. Composable dengan animasi padding-nya

Untuk menganimasikan padding composable, gunakan animateDpAsState yang digabungkan dengan Modifier.padding():

var toggled by remember {
    mutableStateOf(false)
}
val animatedPadding by animateDpAsState(
    if (toggled) {
        0.dp
    } else {
        20.dp
    },
    label = "padding"
)
Box(
    modifier = Modifier
        .aspectRatio(1f)
        .fillMaxSize()
        .padding(animatedPadding)
        .background(Color(0xff53D9A1))
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            toggled = !toggled
        }
)

Menganimasikan elevasi composable

Gambar 8. Elevasi composable yang dianimasikan saat diklik

Untuk menganimasikan elevasi composable, gunakan animateDpAsState yang digabungkan dengan Modifier.graphicsLayer{ }. Untuk perubahan elevasi satu kali, gunakan Modifier.shadow(). Jika Anda menganimasikan bayangan, menggunakan pengubah Modifier.graphicsLayer{ } adalah opsi yang lebih berperforma.

val mutableInteractionSource = remember {
    MutableInteractionSource()
}
val pressed = mutableInteractionSource.collectIsPressedAsState()
val elevation = animateDpAsState(
    targetValue = if (pressed.value) {
        32.dp
    } else {
        8.dp
    },
    label = "elevation"
)
Box(
    modifier = Modifier
        .size(100.dp)
        .align(Alignment.Center)
        .graphicsLayer {
            this.shadowElevation = elevation.value.toPx()
        }
        .clickable(interactionSource = mutableInteractionSource, indication = null) {
        }
        .background(colorGreen)
) {
}

Atau, gunakan composable Card, dan tetapkan properti elevasi ke nilai yang berbeda per status.

Menganimasikan skala, terjemahan, atau rotasi teks

Composable teks yang mengatakan
Gambar 9. Animasi teks yang lancar di antara dua ukuran

Saat menganimasikan skala, terjemahan, atau rotasi teks, tetapkan parameter textMotion di TextStyle ke TextMotion.Animated. Hal ini memastikan transisi yang lebih lancar di antara animasi teks. Gunakan Modifier.graphicsLayer{ } untuk menerjemahkan, memutar, atau menskalakan teks.

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 8f,
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "scale"
)
Box(modifier = Modifier.fillMaxSize()) {
    Text(
        text = "Hello",
        modifier = Modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                transformOrigin = TransformOrigin.Center
            }
            .align(Alignment.Center),
        // Text composable does not take TextMotion as a parameter.
        // Provide it via style argument but make sure that we are copying from current theme
        style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated)
    )
}

Menganimasikan warna teks

Kata-kata
Gambar 10. Contoh yang menampilkan animasi warna teks

Untuk menganimasikan warna teks, gunakan lambda color pada composable BasicText:

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val animatedColor by infiniteTransition.animateColor(
    initialValue = Color(0xFF60DDAD),
    targetValue = Color(0xFF4285F4),
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "color"
)

BasicText(
    text = "Hello Compose",
    color = {
        animatedColor
    },
    // ...
)

Beralih antar-jenis konten

Ucapan layar hijau
Gambar 11. Menggunakan AnimatedContent untuk menganimasikan perubahan antar-composable yang berbeda (diperlambat)

Gunakan AnimatedContent untuk menganimasikan antar-composable yang berbeda. Jika Anda hanya ingin memudar secara standar di antara composable, gunakan Crossfade.

var state by remember {
    mutableStateOf(UiState.Loading)
}
AnimatedContent(
    state,
    transitionSpec = {
        fadeIn(
            animationSpec = tween(3000)
        ) togetherWith fadeOut(animationSpec = tween(3000))
    },
    modifier = Modifier.clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = null
    ) {
        state = when (state) {
            UiState.Loading -> UiState.Loaded
            UiState.Loaded -> UiState.Error
            UiState.Error -> UiState.Loading
        }
    },
    label = "Animated Content"
) { targetState ->
    when (targetState) {
        UiState.Loading -> {
            LoadingScreen()
        }
        UiState.Loaded -> {
            LoadedScreen()
        }
        UiState.Error -> {
            ErrorScreen()
        }
    }
}

AnimatedContent dapat disesuaikan untuk menampilkan berbagai jenis transisi masuk dan keluar. Untuk mengetahui informasi selengkapnya, baca dokumentasi tentang AnimatedContent atau baca postingan blog ini tentang AnimatedContent.

Menganimasikan saat menavigasi ke tujuan yang berbeda

Dua composable, satu hijau bertuliskan Landing dan satu biru bertuliskan Detail, dianimasikan dengan menggeser composable detail ke atas composable landing.
Gambar 12. Menganimasikan antar-composable menggunakan navigation-compose

Untuk menganimasikan transisi antar-composable saat menggunakan artefak navigation-compose, tentukan enterTransition dan exitTransition pada composable. Anda juga dapat menetapkan animasi default untuk digunakan untuk semua tujuan di NavHost level atas:

val navController = rememberNavController()
NavHost(
    navController = navController, startDestination = "landing",
    enterTransition = { EnterTransition.None },
    exitTransition = { ExitTransition.None }
) {
    composable("landing") {
        ScreenLanding(
            // ...
        )
    }
    composable(
        "detail/{photoUrl}",
        arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }),
        enterTransition = {
            fadeIn(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideIntoContainer(
                animationSpec = tween(300, easing = EaseIn),
                towards = AnimatedContentTransitionScope.SlideDirection.Start
            )
        },
        exitTransition = {
            fadeOut(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideOutOfContainer(
                animationSpec = tween(300, easing = EaseOut),
                towards = AnimatedContentTransitionScope.SlideDirection.End
            )
        }
    ) { backStackEntry ->
        ScreenDetails(
            // ...
        )
    }
}

Ada banyak jenis transisi masuk dan keluar yang menerapkan efek yang berbeda pada konten masuk dan keluar, lihat dokumentasi untuk mengetahui informasi selengkapnya.

Mengulangi animasi

Latar belakang hijau yang berubah menjadi latar belakang biru, tanpa batas dengan menganimasikan antara dua warna.
Gambar 13. Warna latar belakang yang dianimasikan antara dua nilai, tanpa batas

Gunakan rememberInfiniteTransition dengan infiniteRepeatable animationSpec untuk terus mengulangi animasi. Ubah RepeatModes untuk menentukan cara bolak-balik.

Gunakan finiteRepeatable untuk mengulangi sejumlah kali yang ditetapkan.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Green,
    targetValue = Color.Blue,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(color)
    }
) {
    // your composable here
}

Memulai animasi saat meluncurkan composable

LaunchedEffect berjalan saat composable memasuki komposisi. Fungsi ini memulai animasi saat meluncurkan composable, Anda dapat menggunakannya untuk mendorong perubahan status animasi. Menggunakan Animatable dengan metode animateTo untuk memulai animasi saat peluncuran:

val alphaAnimation = remember {
    Animatable(0f)
}
LaunchedEffect(Unit) {
    alphaAnimation.animateTo(1f)
}
Box(
    modifier = Modifier.graphicsLayer {
        alpha = alphaAnimation.value
    }
)

Membuat animasi berurutan

Empat lingkaran dengan panah hijau yang dianimasikan di antara setiap lingkaran, dianimasikan satu per satu secara berurutan.
Gambar 14. Diagram yang menunjukkan progres animasi berurutan, satu per satu.

Gunakan API coroutine Animatable untuk melakukan animasi berurutan atau serentak. Memanggil animateTo pada Animatable satu per satu menyebabkan setiap animasi menunggu animasi sebelumnya selesai sebelum melanjutkan . Hal ini karena fungsi ini adalah fungsi penangguhan.

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    alphaAnimation.animateTo(1f)
    yAnimation.animateTo(100f)
    yAnimation.animateTo(500f, animationSpec = tween(100))
}

Membuat animasi serentak

Tiga lingkaran dengan panah hijau yang dianimasikan ke setiap lingkaran, dianimasikan secara bersamaan.
Gambar 15. Diagram yang menunjukkan progres animasi serentak, semuanya secara bersamaan.

Gunakan API coroutine (Animatable#animateTo() atau animate), atau Transition API untuk mencapai animasi serentak. Jika Anda menggunakan beberapa fungsi peluncuran dalam konteks coroutine, fungsi tersebut akan meluncurkan animasi secara bersamaan:

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    launch {
        alphaAnimation.animateTo(1f)
    }
    launch {
        yAnimation.animateTo(100f)
    }
}

Anda dapat menggunakan updateTransition API untuk menggunakan status yang sama guna mendorong banyak animasi properti yang berbeda secara bersamaan. Contoh di bawah menganimasikan dua properti yang dikontrol oleh perubahan status, rect dan borderWidth:

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "transition")

val rect by transition.animateRect(label = "rect") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "borderWidth") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

Mengoptimalkan performa animasi

Animasi di Compose dapat menyebabkan masalah performa. Hal ini disebabkan oleh sifat animasi: memindahkan atau mengubah piksel di layar dengan cepat, frame demi frame untuk menciptakan ilusi gerakan.

Pertimbangkan berbagai fase Compose: komposisi, tata letak, dan gambar. Jika animasi Anda mengubah fase tata letak, semua composable yang terpengaruh harus melakukan tata letak ulang dan menggambar ulang. Jika animasi Anda terjadi dalam fase gambar, secara default animasi akan memiliki performa yang lebih baik daripada jika Anda menjalankan animasi dalam fase tata letak, karena animasi akan memiliki lebih sedikit pekerjaan yang harus dilakukan secara keseluruhan.

Untuk memastikan aplikasi Anda melakukan sesedikit mungkin saat menganimasikan, pilih versi lambda Modifier jika memungkinkan. Tindakan ini akan melewati rekomposisi dan menjalankan animasi di luar fase komposisi. Jika tidak, gunakan Modifier.graphicsLayer{ }, karena pengubah ini selalu berjalan dalam fase gambar. Untuk informasi selengkapnya tentang hal ini, lihat bagian menunda pembacaan dalam dokumentasi performa.

Mengubah pengaturan waktu animasi

Compose secara default menggunakan animasi pegas untuk sebagian besar animasi. Pegas, atau animasi berbasis fisika, terasa lebih alami. Fungsi ini juga dapat diinterupsi karena mengambil kecepatan objek saat ini, bukan waktu tetap. Jika Anda ingin mengganti setelan default, semua API animasi yang ditunjukkan di atas memiliki kemampuan untuk menetapkan animationSpec guna menyesuaikan cara animasi berjalan, baik Anda ingin animasi dijalankan selama durasi tertentu atau lebih berayun.

Berikut adalah ringkasan berbagai opsi animationSpec:

  • spring: Animasi berbasis fisika, yang merupakan default untuk semua animasi. Anda dapat mengubah stiffness atau dampingRatio untuk mendapatkan tampilan dan nuansa animasi yang berbeda.
  • tween (singkatan dari between): Animasi berbasis durasi, menganimasikan antara dua nilai dengan fungsi Easing.
  • keyframes: Spesifikasi untuk menentukan nilai pada titik kunci tertentu dalam animasi.
  • repeatable: Spesifikasi berbasis durasi yang berjalan beberapa kali, yang ditentukan oleh RepeatMode.
  • infiniteRepeatable: Spesifikasi berbasis durasi yang berjalan selamanya.
  • snap: Langsung beralih ke nilai akhir tanpa animasi apa pun.
Tulis teks alternatif Anda di sini
Gambar 16. Tidak ada kumpulan spesifikasi vs. Kumpulan spesifikasi Spring Kustom

Baca dokumentasi selengkapnya untuk mengetahui informasi selengkapnya tentang animationSpecs.

Referensi lainnya

Untuk contoh animasi seru lainnya di Compose, lihat hal berikut: