Jetpack Compose menyediakan API yang andal dan dapat diperluas yang memudahkan penerapan berbagai animasi di UI aplikasi Anda. Dokumen ini menjelaskan cara menggunakan API ini serta API mana yang sebaiknya digunakan sesuai skenario animasi Anda.
Ringkasan
Animasi sangat penting dalam aplikasi seluler modern untuk menghadirkan pengalaman pengguna yang lancar dan dapat dipahami. Banyak Animation API Jetpack Compose tersedia sebagai fungsi composable, seperti tata letak dan elemen UI lainnya, dan didukung oleh API tingkat rendah yang dibuat dengan fungsi penangguhan coroutine Kotlin. Panduan ini dimulai dengan API tingkat tinggi yang berguna dalam banyak skenario praktis, dan selanjutnya menjelaskan API tingkat rendah yang memberi Anda kontrol dan penyesuaian lebih lanjut.
Diagram di bawah ini membantu Anda menentukan API apa yang akan digunakan untuk menerapkan animasi.
- Jika Anda menganimasikan perubahan konten dalam tata letak:
- Jika Anda menganimasikan pemunculan dan penghilangan:
- Gunakan
AnimatedVisibility
.
- Gunakan
- Menukar konten berdasarkan status:
- Jika Anda melakukan crossfading konten:
- Gunakan
Crossfade
.
- Gunakan
- Jika tidak, gunakan
AnimatedContent
.
- Jika Anda melakukan crossfading konten:
- Jika tidak, gunakan
Modifier.animateContentSize
.
- Jika Anda menganimasikan pemunculan dan penghilangan:
- Jika animasi berbasis status:
- Jika animasi terjadi selama komposisi:
- Jika animasi tidak terbatas:
- Gunakan
rememberInfiniteTransition
.
- Gunakan
- Jika Anda menganimasikan beberapa nilai secara bersamaan:
- Gunakan
updateTransition
.
- Gunakan
- Jika tidak, gunakan
animate*AsState
.
- Jika animasi tidak terbatas:
- Jika animasi terjadi selama komposisi:
- Jika Anda ingin memiliki kontrol yang mendetail terhadap waktu animasi:
- Gunakan
Animation
, sepertiTargetBasedAnimation
atauDecayAnimation
.
- Gunakan
- Jika animasi adalah satu-satunya sumber ketepatan
- Gunakan
Animatable
.
- Gunakan
- Jika tidak, gunakan
AnimationState
atauanimate
.
API animasi tingkat tinggi
Compose menawarkan API animasi tingkat tinggi untuk beberapa pola animasi umum yang digunakan di banyak aplikasi. API ini dibuat khusus agar sesuai dengan praktik terbaik Gerakan Desain Material.
AnimatedVisibility
Composable
AnimatedVisibility
menganimasikan muncul dan hilangnya kontennya.
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}
Secara default, konten muncul dengan menjadi jelas dan meluas, serta menghilang dengan memudar dan menyusut. Transisi dapat disesuaikan dengan menentukan EnterTransition
dan ExitTransition
.
var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
visible = visible,
enter = slideInVertically {
// Slide in from 40 dp from the top.
with(density) { -40.dp.roundToPx() }
} + expandVertically(
// Expand from the top.
expandFrom = Alignment.Top
) + fadeIn(
// Fade in with the initial alpha of 0.3f.
initialAlpha = 0.3f
),
exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
Text("Hello", Modifier.fillMaxWidth().height(200.dp))
}
Seperti yang Anda lihat pada contoh di atas, Anda dapat menggabungkan beberapa objek EnterTransition
atau ExitTransition
dengan operator +
, dan masing-masing menerima parameter opsional untuk menyesuaikan perilakunya. Lihat referensi untuk informasi selengkapnya.
Contoh EnterTransition
dan ExitTransition
AnimatedVisibility
juga menawarkan varian yang memerlukan
MutableTransitionState
. Hal ini memungkinkan Anda untuk memicu animasi segera setelah
AnimatedVisibility
ditambahkan ke hierarki komposisi. Hal ini juga berguna untuk
mengamati status animasi.
// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
MutableTransitionState(false).apply {
// Start the animation immediately.
targetState = true
}
}
Column {
AnimatedVisibility(visibleState = state) {
Text(text = "Hello, world!")
}
// Use the MutableTransitionState to know the current animation state
// of the AnimatedVisibility.
Text(
text = when {
state.isIdle && state.currentState -> "Visible"
!state.isIdle && state.currentState -> "Disappearing"
state.isIdle && !state.currentState -> "Invisible"
else -> "Appearing"
}
)
}
Menganimasikan masuk dan keluar untuk turunan
Konten dalam AnimatedVisibility
(turunan langsung atau tidak langsung) dapat menggunakan pengubah
animateEnterExit
untuk menentukan perilaku animasi yang berbeda bagi setiap turunan. Efek visual
untuk setiap turunan ini adalah kombinasi animasi yang ditetapkan
pada composable AnimatedVisibility
serta animasi masuk dan
keluar turunan itu sendiri.
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
// Fade in/out the background and the foreground.
Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
Box(
Modifier
.align(Alignment.Center)
.animateEnterExit(
// Slide in/out the inner box.
enter = slideInVertically(),
exit = slideOutVertically()
)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp)
.background(Color.Red)
) {
// Content of the notification…
}
}
}
Dalam beberapa kasus, Anda mungkin ingin AnimatedVisibility
tidak menerapkan animasi sama sekali
sehingga turunan dapat memiliki animasinya sendiri dengan menggunakan
animateEnterExit
. Untuk mencapai hal ini, tentukan EnterTransition.None
dan
ExitTransition.None
pada composable AnimatedVisibility
.
Menambahkan animasi kustom
Jika Anda ingin menambahkan efek animasi kustom di luar animasi masuk dan keluar
bawaan, akses instance Transition
dasar melalui properti
transition
di dalam lambda konten untuk AnimatedVisibility
. Semua status
animasi yang ditambahkan ke instance Transition akan berjalan bersamaan dengan animasi
masuk dan keluar dari AnimatedVisibility
. AnimatedVisibility
menunggu hingga
semua animasi di Transition
selesai sebelum menghapus kontennya.
Untuk animasi keluar yang dibuat terpisah dari Transition
(seperti menggunakan
animate*AsState
), AnimatedVisibility
tidak akan dapat memperhitungkannya,
dan oleh karena itu dapat menghapus composable konten sebelum selesai.
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) { // this: AnimatedVisibilityScope
// Use AnimatedVisibilityScope#transition to add a custom animation
// to the AnimatedVisibility.
val background by transition.animateColor { state ->
if (state == EnterExitState.Visible) Color.Blue else Color.Gray
}
Box(modifier = Modifier.size(128.dp).background(background))
}
Lihat updateTransition untuk mengetahui detail tentang Transition
.
animate*AsState
Fungsi animate*AsState
adalah API animasi yang paling sederhana di Compose untuk menganimasikan satu nilai. Anda hanya memberikan nilai akhir (atau nilai target), dan API akan memulai animasi dari nilai saat ini ke nilai yang ditentukan.
Berikut contoh menganimasikan alfa menggunakan API ini. Hanya dengan menggabungkan
nilai target di animateFloatAsState
, nilai alfa sekarang menjadi nilai animasi di antara nilai yang disediakan (1f
maupun 0.5f
dalam kasus ini).
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
Modifier.fillMaxSize()
.graphicsLayer(alpha = alpha)
.background(Color.Red)
)
Perhatikan bahwa Anda tidak perlu membuat instance dari kelas animasi, atau menangani gangguan. Di balik layar, objek animasi (yaitu, instance Animatable
) akan dibuat dan diingat di situs panggilan, dengan nilai target pertama sebagai nilai awal. Sejak saat itu, setiap kali Anda memberikan nilai target yang berbeda untuk composable ini, animasi akan otomatis dimulai terhadap nilai tersebut. Jika saat ini sudah ada animasi, animasi dimulai dari nilai saat ini (dan kecepatannya) dan bergerak menuju nilai target. Selama
animasi, composable ini dapat dikomposisi ulang dan menampilkan nilai animasi yang diupdate setiap frame.
Secara mandiri, Compose menyediakan fungsi animate*AsState
untuk Float
,
Color
, Dp
, Size
, Offset
, Rect
, Int
, IntOffset
, dan
IntSize
. Anda dapat dengan mudah menambahkan dukungan untuk jenis data lainnya dengan memberikan
TwoWayConverter
ke animateValueAsState
yang menggunakan jenis umum.
Anda dapat menyesuaikan spesifikasi animasi dengan menyediakan AnimationSpec
.
Lihat AnimationSpec untuk informasi selengkapnya.
AnimatedContent (eksperimental)
Composable AnimatedContent
akan menganimasikan kontennya saat composable tersebut berubah berdasarkan
status target.
Row {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Add")
}
AnimatedContent(targetState = count) { targetCount ->
// Make sure to use `targetCount`, not `count`.
Text(text = "Count: $targetCount")
}
}
Perhatikan bahwa Anda harus selalu menggunakan parameter lambda dan mencerminkannya ke konten. API menggunakan nilai ini sebagai kunci untuk mengidentifikasi konten yang saat ini ditampilkan.
Secara default, konten awal akan memudar, lalu konten target akan makin jelas
(perilaku ini disebut memudar). Anda
dapat menyesuaikan perilaku animasi ini dengan menentukan objek ContentTransform
ke
parameter transitionSpec
. Anda dapat membuat ContentTransform
dengan mengombinasikan
EnterTransition
dan ExitTransition
menggunakan fungsi infix with
. Anda dapat menerapkan SizeTransform
ke ContentTransform
dengan melampirkannya dengan
fungsi infix using
.
AnimatedContent(
targetState = count,
transitionSpec = {
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
}
) { targetCount ->
Text(text = "$targetCount")
}
EnterTransition
akan menentukan cara konten target akan muncul, dan
ExitTransition
akan menentukan cara konten awal akan menghilang. Selain
semua fungsi EnterTransition
dan ExitTransition
yang tersedia untuk
AnimatedVisibility
, AnimatedContent
menawarkan slideIntoContainer
dan slideOutOfContainer
.
Fungsi ini merupakan alternatif yang praktis untuk slideInHorizontally/Vertically
dan
slideOutHorizontally/Vertically
yang menghitung jarak slide berdasarkan pada
ukuran konten awal dan konten target dari
konten AnimatedContent
.
SizeTransform
menentukan cara
ukuran akan dianimasikan antara konten awal dan target. Anda memiliki
akses ke ukuran awal dan ukuran target saat membuat
animasi. SizeTransform
juga mengontrol apakah konten harus dipotong
ke ukuran komponen selama animasi.
var expanded by remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colors.primary,
onClick = { expanded = !expanded }
) {
AnimatedContent(
targetState = expanded,
transitionSpec = {
fadeIn(animationSpec = tween(150, 150)) with
fadeOut(animationSpec = tween(150)) using
SizeTransform { initialSize, targetSize ->
if (targetState) {
keyframes {
// Expand horizontally first.
IntSize(targetSize.width, initialSize.height) at 150
durationMillis = 300
}
} else {
keyframes {
// Shrink vertically first.
IntSize(initialSize.width, targetSize.height) at 150
durationMillis = 300
}
}
}
}
) { targetExpanded ->
if (targetExpanded) {
Expanded()
} else {
ContentIcon()
}
}
}
Menganimasikan masuk/keluar untuk turunan
Sama seperti AnimatedVisibility
, pengubah animateEnterExit
tersedia di dalam lambda konten AnimatedContent
. Gunakan ini
untuk menerapkan EnterAnimation
dan ExitAnimation
ke setiap turunan langsung atau tidak langsung
secara terpisah.
Menambahkan animasi kustom
Sama seperti AnimatedVisibility
, kolom transition
tersedia di dalam
lambda konten AnimatedContent
. Gunakan ini untuk membuat efek animasi
kustom yang berjalan secara bersamaan dengan transisi AnimatedContent
. Lihat
updateTransition untuk mengetahui detailnya.
animateContentSize
Pengubah animateContentSize
menganimasikan perubahan ukuran.
var message by remember { mutableStateOf("Hello") }
Box(
modifier = Modifier.background(Color.Blue).animateContentSize()
) {
Text(text = message)
}
Crossfade
Crossfade
membuat animasi di antara dua tata letak dengan animasi crossfade. Dengan mengganti
nilai yang diteruskan ke parameter current
, konten dialihkan dengan
animasi crossfade.
var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
when (screen) {
"A" -> Text("Page A")
"B" -> Text("Page B")
}
}
updateTransition
Transition
mengelola satu atau beberapa animasi sebagai turunannya dan menjalankannya secara bersamaan di beberapa status.
Status dapat berupa jenis data apa pun. Dalam banyak kasus, Anda dapat menggunakan jenis enum
kustom untuk memastikan keamanan jenis, seperti dalam contoh berikut:
enum class BoxState {
Collapsed,
Expanded
}
updateTransition
membuat dan mengingat instance Transition
dan memperbarui statusnya.
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState)
Anda kemudian dapat menggunakan salah satu dari fungsi ekstensi animate*
untuk menentukan animasi turunan dalam transisi ini. Menentukan nilai target untuk setiap status.
Fungsi animate*
ini menampilkan nilai animasi yang diperbarui setiap frame selama animasi saat status transisi diperbarui dengan updateTransition
.
val rect by transition.animateRect { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
val borderWidth by transition.animateDp { state ->
when (state) {
BoxState.Collapsed -> 1.dp
BoxState.Expanded -> 0.dp
}
}
Secara opsional, Anda dapat meneruskan parameter transitionSpec
untuk menentukan AnimationSpec
yang berbeda untuk setiap kombinasi perubahan status transisi. Lihat
AnimationSpec untuk mengetahui informasi selengkapnya.
val color by transition.animateColor(
transitionSpec = {
when {
BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
spring(stiffness = 50f)
else ->
tween(durationMillis = 500)
}
}
) { state ->
when (state) {
BoxState.Collapsed -> MaterialTheme.colors.primary
BoxState.Expanded -> MaterialTheme.colors.background
}
}
Setelah transisi mencapai status target, Transition.currentState
akan sama dengan Transition.targetState
. Ini dapat digunakan sebagai sinyal
apakah transisi telah selesai.
Kita terkadang ingin memiliki status awal yang berbeda dengan status target pertama. Kita dapat menggunakan updateTransition
dengan MutableTransitionState
untuk mencapainya. Misalnya, memungkinkan kita memulai animasi segera setelah kode memasuki
komposisi.
// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(currentState)
// ...
Untuk transisi lebih kompleks yang melibatkan beberapa fungsi composable, Anda dapat
menggunakan createChildTransition
untuk membuat transisi turunan. Teknik ini berguna untuk memisahkan masalah
di antara beberapa subkomponen dalam composable yang kompleks. Transisi induk akan
memperhatikan semua nilai animasi di transisi turunan.
enum class DialerState { DialerMinimized, NumberPad }
@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
// `isVisibleTransition` spares the need for the content to know
// about other DialerStates. Instead, the content can focus on
// animating the state change between visible and not visible.
}
@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
// `isVisibleTransition` spares the need for the content to know
// about other DialerStates. Instead, the content can focus on
// animating the state change between visible and not visible.
}
@Composable
fun Dialer(dialerState: DialerState) {
val transition = updateTransition(dialerState)
Box {
// Creates separate child transitions of Boolean type for NumberPad
// and DialerButton for any content animation between visible and
// not visible
NumberPad(
transition.createChildTransition {
it == DialerState.NumberPad
}
)
DialerButton(
transition.createChildTransition {
it == DialerState.DialerMinimized
}
)
}
}
Menggunakan transisi dengan AnimatedVisibility dan AnimatedContent
AnimatedVisibility
dan AnimatedContent
tersedia sebagai fungsi ekstensi dari Transition
. targetState
untuk
Transition.AnimatedVisibility
dan Transition.AnimatedContent
berasal
dari Transition
, dan memicu transisi masuk/keluar sesuai kebutuhan saat
targetState
Transition
telah berubah. Fungsi ekstensi ini memungkinkan semua
animasi enter/exit/sizeTransform yang akan bersifat internal ke
AnimatedVisibility
/AnimatedContent
diangkat ke dalam Transition
.
Dengan fungsi ekstensi ini, perubahan status AnimatedVisibility
/AnimatedContent
dapat diamati dari luar. Sebagai ganti parameter visible
boolean,
versi AnimatedVisibility
ini menggunakan lambda yang mengonversi status target transisi induk
ke dalam boolean.
Lihat AnimatedVisibility dan AnimatedContent untuk mengetahui detailnya.
var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected)
val borderColor by transition.animateColor { isSelected ->
if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp { isSelected ->
if (isSelected) 10.dp else 2.dp
}
Surface(
onClick = { selected = !selected },
shape = RoundedCornerShape(8.dp),
border = BorderStroke(2.dp, borderColor),
elevation = elevation
) {
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Text(text = "Hello, world!")
// AnimatedVisibility as a part of the transition.
transition.AnimatedVisibility(
visible = { targetSelected -> targetSelected },
enter = expandVertically(),
exit = shrinkVertically()
) {
Text(text = "It is fine today.")
}
// AnimatedContent as a part of the transition.
transition.AnimatedContent { targetState ->
if (targetState) {
Text(text = "Selected")
} else {
Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
}
}
}
}
Melakukan enkapsulasi Transisi dan membuatnya dapat digunakan kembali
Untuk kasus penggunaan sederhana, menentukan animasi transisi dalam composable yang sama dengan UI adalah opsi yang sangat valid. Namun, saat Anda mengerjakan komponen yang rumit dengan sejumlah nilai animasi, sebaiknya pisahkan penerapan animasi dari composable UI.
Anda dapat melakukannya dengan membuat class yang menyimpan semua nilai animasi dan fungsi 'update' yang menampilkan instance class tersebut. Penerapan transisi dapat diekstrak ke fungsi terpisah yang baru. Pola ini berguna saat ada kebutuhan untuk memusatkan logika animasi, atau membuat animasi yang rumit dapat digunakan kembali.
enum class BoxState { Collapsed, Expanded }
@Composable
fun AnimatingBox(boxState: BoxState) {
val transitionData = updateTransitionData(boxState)
// UI tree
Box(
modifier = Modifier
.background(transitionData.color)
.size(transitionData.size)
)
}
// Holds the animation values.
private class TransitionData(
color: State<Color>,
size: State<Dp>
) {
val color by color
val size by size
}
// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
val transition = updateTransition(boxState)
val color = transition.animateColor { state ->
when (state) {
BoxState.Collapsed -> Color.Gray
BoxState.Expanded -> Color.Red
}
}
val size = transition.animateDp { state ->
when (state) {
BoxState.Collapsed -> 64.dp
BoxState.Expanded -> 128.dp
}
}
return remember(transition) { TransitionData(color, size) }
}
rememberInfiniteTransition
InfiniteTransition
memiliki satu atau beberapa animasi turunan seperti Transition
, tetapi animasi akan mulai berjalan segera setelah dimasukkan ke dalam komposisi dan tidak berhenti kecuali dihapus. Anda dapat membuat instance InfiniteTransition
dengan rememberInfiniteTransition
. Animasi turunan dapat ditambahkan dengan animateColor
, animatedFloat
, atau animatedValue
. Anda juga perlu menentukan
infiniteRepeatable untuk menentukan spesifikasi
animasi.
val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(Modifier.fillMaxSize().background(color))
Animation API tingkat rendah
Semua API animasi tingkat tinggi yang disebutkan di bagian sebelumnya dibuat berdasarkan fondasi API animasi tingkat rendah.
Fungsi animate*AsState
adalah API yang paling sederhana, yang merender perubahan nilai instan sebagai nilai animasi. Ini didukung oleh Animatable
, yang merupakan API berbasis coroutine untuk menganimasikan nilai tunggal. updateTransition
membuat objek transisi yang dapat mengelola beberapa nilai animasi dan menjalankannya berdasarkan perubahan status. rememberInfiniteTransition
serupa, tetapi membuat
transisi tidak terbatas yang dapat mengelola beberapa animasi yang terus berjalan
tanpa batas. Semua API ini merupakan composable kecuali Animatable
, yang berarti animasi ini dapat dibuat di luar komposisi.
Semua API ini didasarkan pada Animation
API yang lebih mendasar. Meskipun sebagian besar aplikasi tidak akan berinteraksi langsung dengan Animation
, beberapa kemampuan penyesuaian untuk Animation
tersedia melalui API tingkat yang lebih tinggi. Lihat Menyesuaikan animasi untuk mengetahui informasi selengkapnya tentang AnimationVector
dan AnimationSpec
.
Animatable
Animatable
adalah pemegang nilai yang dapat menganimasikan nilai karena diubah melalui animateTo
. Ini adalah API yang mencadangkan penerapan animate*AsState
.
Ini memastikan kelanjutan yang konsisten dan pengalaman eksklusif, yang berarti bahwa perubahan nilai selalu berkelanjutan dan setiap animasi yang sedang berlangsung akan dibatalkan.
Banyak fitur Animatable
, termasuk animateTo
, disediakan sebagai fungsi penangguhan. Ini berarti bahwa fitur-fitur tersebut harus digabungkan dalam cakupan coroutine yang sesuai. Misalnya, Anda dapat menggunakan composable LaunchedEffect
untuk membuat cakupan hanya selama durasi nilai kunci yang ditentukan.
// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(Modifier.fillMaxSize().background(color.value))
Pada contoh di atas, kita membuat dan mengingat instance Animatable
dengan nilai awal Color.Gray
. Bergantung pada nilai flag boolean ok
, warna animasi ke Color.Green
atau Color.Red
. Setiap perubahan berikutnya pada nilai boolean akan memulai animasi ke warna lainnya. Jika ada animasi yang berlangsung saat nilai diubah, animasi akan dibatalkan, dan animasi baru dimulai dari nilai snapshot saat ini dengan kecepatan saat ini.
Ini adalah implementasi animasi yang mencadangkan API animate*AsState
yang disebutkan di bagian sebelumnya. Dibandingkan dengan animate*AsState
, menggunakan
Animatable
secara langsung akan memberi kita kontrol yang lebih mendetail. Pertama,
Animatable
dapat memiliki nilai awal yang berbeda dari nilai target pertamanya.
Misalnya, contoh kode di atas menampilkan kotak abu-abu terlebih dahulu, yang segera mulai beranimasi menjadi hijau atau merah. Kedua, Animatable
memberikan lebih banyak
operasi pada nilai konten, yaitu snapTo
dan animateDecay
. snapTo
segera menyetel nilai saat ini ke nilai target. Hal ini berguna saat
animasi itu sendiri bukan satu-satunya sumber ketepatan dan harus disinkronkan dengan status
lain, seperti peristiwa sentuh. animateDecay
memulai animasi yang melambat
dari kecepatan yang ditentukan. Ini berguna untuk menerapkan perilaku fling. Lihat
Gestur dan animasi untuk informasi selengkapnya.
Secara mandiri, Animatable
mendukung Float
dan Color
, namun semua jenis data dapat digunakan dengan menyediakan TwoWayConverter
. Lihat
AnimationVector untuk mengetahui informasi selengkapnya.
Anda dapat menyesuaikan spesifikasi animasi dengan menyediakan AnimationSpec
.
Lihat AnimationSpec untuk informasi selengkapnya.
Animasi
Animation
adalah Animation API tingkat terendah yang tersedia. Banyak animasi
yang telah kita lihat sejauh ini dibuat di atas Animasi. Ada dua subjenis Animation
:
TargetBasedAnimation
dan DecayAnimation
.
Animation
hanya boleh digunakan untuk mengontrol waktu animasi secara manual.
Animation
bersifat stateless, dan tidak memiliki konsep siklus proses apa pun. Hal ini
berfungsi sebagai mesin hitung animasi yang digunakan pada API dengan tingkat yang lebih tinggi.
TargetBasedAnimation
API lain mencakup sebagian besar kasus penggunaan, namun penggunaan TargetBasedAnimation
secara langsung
memungkinkan Anda untuk mengontrol waktu pemutaran animasi sendiri. Pada contoh di bawah,
waktu pemutaran TargetAnimation
dikontrol secara manual berdasarkan waktu render
frame yang disediakan oleh withFrameNanos
.
val anim = remember {
TargetBasedAnimation(
animationSpec = tween(200),
typeConverter = Float.VectorConverter,
initialValue = 200f,
targetValue = 1000f
)
}
var playTime by remember { mutableStateOf(0L) }
LaunchedEffect(anim) {
val startTime = withFrameNanos { it }
do {
playTime = withFrameNanos { it } - startTime
val animationValue = anim.getValueFromNanos(playTime)
} while (someCustomCondition())
}
DecayAnimation
Tidak seperti TargetBasedAnimation
,
DecayAnimation
tidak memerlukan targetValue
yang disediakan. Sebaliknya, elemen ini menghitung
targetValue
berdasarkan kondisi awal, yang ditetapkan oleh initialVelocity
dan
initialValue
serta DecayAnimationSpec
yang disediakan.
Animasi decay sering digunakan setelah gestur ayunkan jari untuk memperlambat elemen hingga
berhenti. Kecepatan animasi dimulai pada nilai yang disetel oleh initialVelocityVector
dan melambat seiring waktu.
Menyesuaikan animasi
Banyak Animation API biasanya menerima parameter untuk menyesuaikan perilakunya.
AnimationSpec
Sebagian besar API animasi memungkinkan developer menyesuaikan spesifikasi animasi dengan parameter AnimationSpec
opsional.
val alpha: Float by animateFloatAsState(
targetValue = if (enabled) 1f else 0.5f,
// Configure the animation duration and easing.
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)
Ada berbagai jenis AnimationSpec
untuk membuat berbagai jenis animasi.
spring
spring
membuat animasi berbasis fisika antara nilai awal dan akhir. Dibutuhkan 2 parameter: dampingRatio
dan stiffness
.
dampingRatio
menentukan seberapa guncangan seharusnya. Nilai defaultnya adalah
Spring.DampingRatioNoBouncy
.
stiffness
menentukan seberapa cepat spring akan bergerak menuju nilai akhir. Nilai
defaultnya adalah Spring.StiffnessMedium
.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
spring
dapat menangani gangguan secara lebih lancar daripada jenis AnimationSpec
berbasis durasi karena dapat menjamin kontinuitas saat target berubah di tengah animasi. spring
digunakan sebagai
AnimationSpec default oleh banyak API animasi, seperti animate*AsState
dan
updateTransition
.
tween
tween
menganimasikan antara nilai awal dan akhir pada durationMillis
yang ditentukan menggunakan kurva easing. Lihat Easing untuk mengetahui informasi selengkapnya. Anda juga dapat menentukan delayMillis
untuk menunda dimulainya animasi.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
keyframes
keyframes
dianimasikan berdasarkan nilai snapshot yang ditentukan pada stempel waktu yang berbeda dalam durasi animasi. Pada waktu tertentu, nilai animasi akan diinterpolasi antara dua nilai keyframe. Untuk setiap keyframe ini, Easing dapat ditentukan untuk menentukan kurva interpolasi.
Menentukan nilai pada 0 md dan pada waktu durasi bersifat opsional. Jika Anda tidak menentukan nilai ini, nilai default akan ditetapkan ke nilai awal dan akhir animasi.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 375
0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
0.4f at 75 // ms
0.4f at 225 // ms
}
)
dapat diulang
repeatable
menjalankan animasi berbasis durasi (seperti tween
atau keyframes
) berulang kali hingga mencapai jumlah iterasi yang ditentukan. Anda dapat meneruskan parameter repeatMode
untuk menentukan apakah animasi harus diulang dengan memulai dari awal (RepeatMode.Restart
) atau dari akhir (RepeatMode.Reverse
).
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
infiniteRepeatable
infiniteRepeatable
seperti repeatable
, tetapi berulang untuk jumlah iterasi yang tak terbatas.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
Dalam pengujian menggunakan ComposeTestRule
, animasi yang menggunakan infiniteRepeatable
tidak dijalankan. Komponen akan dirender menggunakan nilai awal setiap nilai animasi.
snap
snap
adalah AnimationSpec
khusus yang langsung mengalihkan nilai ke nilai akhir. Anda dapat menentukan delayMillis
untuk menunda awal animasi.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = snap(delayMillis = 50)
)
Easing
Operasi AnimationSpec
berbasis durasi (seperti tween
atau keyframes
) menggunakan Easing
untuk menyesuaikan fraksi animasi. Dengan cara ini, nilai animasi dapat dipercepat dan diperlambat, dibandingkan bergerak pada tingkat konstan. Fraksi adalah nilai antara 0 (awal) dan 1,0 (akhir) yang menunjukkan titik saat ini dalam animasi.
Easing sebenarnya merupakan fungsi yang mengambil nilai fraksi antara 0 dan 1,0 dan menampilkan float. Nilai yang ditampilkan dapat berada di luar batas untuk mewakili overshoot atau undershoot. Easing kustom dapat dibuat seperti kode di bawah.
val CustomEasing = Easing { fraction -> fraction * fraction }
@Composable
fun EasingUsage() {
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
easing = CustomEasing
)
)
// ...
}
Compose menyediakan beberapa fungsi Easing
bawaan yang mencakup sebagian besar kasus penggunaan.
Lihat Kecepatan - Desain
Material untuk mengetahui
informasi selengkapnya tentang Easing yang akan digunakan bergantung pada skenario Anda.
FastOutSlowInEasing
LinearOutSlowInEasing
FastOutLinearEasing
LinearEasing
CubicBezierEasing
- Selengkapnya
AnimationVector
Sebagian besar Compose API animasi mendukung Float
, Color
, Dp
, dan jenis data dasar lainnya sebagai nilai animasi secara mandiri, tetapi terkadang Anda perlu menganimasikan jenis data lain termasuk yang khusus. Selama animasi, nilai animasi apa pun akan direpresentasikan sebagai AnimationVector
. Nilai ini dikonversi menjadi AnimationVector
dan sebaliknya oleh TwoWayConverter
yang terkait sehingga sistem animasi inti dapat menanganinya secara seragam. Misalnya, Int
direpresentasikan sebagai AnimationVector1D
yang memiliki satu nilai float.
TwoWayConverter
untuk Int
terlihat seperti ini:
val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
Color
pada dasarnya adalah kumpulan 4 nilai, merah, hijau, biru, dan alfa, sehingga Color
dikonversi menjadi AnimationVector4D
yang menyimpan 4 nilai float. Dengan cara ini, setiap jenis data yang digunakan dalam animasi dikonversi menjadi AnimationVector1D
, AnimationVector2D
, AnimationVector3D
, atau AnimationVector4D
bergantung pada dimensinya. Hal ini memungkinkan berbagai
komponen objek dianimasikan secara independen, masing-masing dengan pelacakan
kecepatannya sendiri. Converter bawaan untuk jenis data dasar dapat diakses
menggunakan Color.VectorConverter
, Dp.VectorConverter
, dan seterusnya.
Jika ingin menambahkan dukungan untuk jenis data baru sebagai nilai animasi, Anda dapat
membuat TwoWayConverter
Anda sendiri dan menyediakannya ke API. Misalnya, Anda
dapat menggunakan animateValueAsState
untuk menganimasikan jenis data kustom seperti ini:
data class MySize(val width: Dp, val height: Dp)
@Composable
fun MyAnimation(targetSize: MySize) {
val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
targetSize,
TwoWayConverter(
convertToVector = { size: MySize ->
// Extract a float value from each of the `Dp` fields.
AnimationVector2D(size.width.value, size.height.value)
},
convertFromVector = { vector: AnimationVector2D ->
MySize(vector.v1.dp, vector.v2.dp)
}
)
)
}
Resource vektor animasi (eksperimental)
Untuk menggunakan resource AnimatedVectorDrawable
, muat file drawable menggunakan animatedVectorResource
dan teruskan boolean
untuk beralih antara status awal dan akhir drawable Anda.
@Composable
fun AnimatedVectorDrawable() {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = "Timer",
modifier = Modifier.clickable {
atEnd = !atEnd
},
contentScale = ContentScale.Crop
)
}
Untuk informasi selengkapnya tentang format file drawable, lihat Menganimasikan grafik drawable.
Animasi item daftar
Jika Anda ingin menganimasikan pengurutan ulang item di dalam daftar atau petak Lambat, lihat dokumentasi animasi item tata letak Lambat.
Gestur dan animasi (lanjutan)
Ada beberapa hal yang perlu dipertimbangkan saat mengerjakan peristiwa sentuh dan animasi, dibandingkan saat kita menangani animasi saja. Terlebih dahulu, kita mungkin perlu menghentikan animasi yang sedang berlangsung saat peristiwa sentuh dimulai karena interaksi pengguna harus diutamakan.
Pada contoh di bawah, kita menggunakan Animatable
untuk mewakili posisi offset komponen lingkaran. Peristiwa sentuh diproses dengan pengubah pointerInput
. Saat mendeteksi peristiwa ketuk baru, kita memanggil animateTo
untuk menganimasi nilai offset ke posisi ketuk. Peristiwa ketuk juga dapat terjadi selama animasi, dan dalam hal ini, animateTo
mengganggu animasi yang sedang berlangsung dan memulai animasi ke posisi target baru sambil mempertahankan kecepatan animasi yang terganggu.
@Composable
fun Gesture() {
val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
coroutineScope {
while (true) {
// Detect a tap event and obtain its position.
val position = awaitPointerEventScope {
awaitFirstDown().position
}
launch {
// Animate to the tap position.
offset.animateTo(position)
}
}
}
}
) {
Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
}
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
Pola lain yang sering terjadi adalah kita perlu menyinkronkan nilai animasi dengan nilai yang berasal dari peristiwa sentuh, seperti tarik. Pada contoh di bawah, kita melihat "geser untuk menutup" diterapkan sebagai Modifier
(bukan menggunakan composable SwipeToDismiss
). Offset horizontal elemen direpresentasikan sebagai Animatable
. API ini memiliki karakteristik yang berguna dalam animasi gestur. Nilainya dapat diubah oleh peristiwa sentuh serta animasi. Saat menerima peristiwa sentuhan, kita menghentikan Animatable
dengan metode stop
, sehingga setiap animasi yang sedang berjalan terhenti.
Selama peristiwa tarik, kita menggunakan snapTo
untuk memperbarui nilai Animatable
dengan nilai yang dihitung dari peristiwa sentuh. Untuk fling, Compose menyediakan VelocityTracker
untuk merekam peristiwa tarik dan menghitung kecepatan. Kecepatan dapat diumpankan langsung ke animateDecay
untuk animasi fling. Saat ingin menggeser nilai offset kembali ke posisi semula, kita menentukan nilai offset target 0f
dengan metode animateTo
.
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate fling decay.
val decay = splineBasedDecay<Float>(this)
// Use suspend functions for touch events and the Animatable.
coroutineScope {
while (true) {
// Detect a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
// Stop any ongoing animation.
offsetX.stop()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch {
offsetX.snapTo(
offsetX.value + change.positionChange().x
)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
// No longer receiving touch events. Prepare the animation.
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(
offsetX.value,
velocity
)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity
)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
Pengujian
Compose menawarkan ComposeTestRule
yang memungkinkan Anda menulis pengujian untuk animasi secara deterministik dengan kontrol penuh selama jam pengujian. Ini memungkinkan Anda memverifikasi nilai animasi menengah. Selain itu, pengujian dapat berjalan lebih cepat dibandingkan durasi animasi yang sebenarnya.
ComposeTestRule
menampilkan jam pengujian sebagai mainClock
. Anda dapat menyetel properti autoAdvance
ke false untuk mengontrol jam di kode pengujian. Setelah memulai animasi yang ingin Anda uji, jam dapat digerakkan dengan advanceTimeBy
.
Satu hal yang perlu diperhatikan di sini adalah advanceTimeBy
tidak menggerakkan jam persis dengan durasi yang ditentukan. Sebaliknya, ini membulatkannya ke durasi terdekat yang
merupakan pengali dari durasi frame.
@get:Rule
val rule = createComposeRule()
@Test
fun testAnimationWithClock() {
// Pause animations
rule.mainClock.autoAdvance = false
var enabled by mutableStateOf(false)
rule.setContent {
val color by animateColorAsState(
targetValue = if (enabled) Color.Red else Color.Green,
animationSpec = tween(durationMillis = 250)
)
Box(Modifier.size(64.dp).background(color))
}
// Initiate the animation.
enabled = true
// Let the animation proceed.
rule.mainClock.advanceTimeBy(50L)
// Compare the result with the image showing the expected result.
// `assertAgainGolden` needs to be implemented in your code.
rule.onRoot().captureToImage().assertAgainstGolden()
}
Dukungan alat
Android Studio mendukung pemeriksaan
updateTransition
dan
animatedVisibility
pada
Pratinjau Animasi. Anda dapat melakukan hal
berikut:
- Melihat pratinjau transisi per frame
- Memeriksa nilai untuk semua animasi dalam transisi
- Melihat pratinjau transisi antara status awal dan target
- Memeriksa dan mengoordinasikan beberapa animasi sekaligus
Saat memulai Pratinjau Animasi, Anda akan melihat panel "Animations" tempat Anda dapat
menjalankan transisi apa pun yang disertakan dalam pratinjau. Transisi serta setiap
nilai animasinya diberi label dengan nama default. Anda dapat menyesuaikan label
dengan menentukan parameter label
di updateTransition
dan
fungsi AnimatedVisibility
. Untuk informasi selengkapnya, lihat
Pratinjau Animasi.
Mempelajari lebih lanjut
Untuk mempelajari animasi di Jetpack Compose lebih lanjut, lihat referensi tambahan berikut: