Komponen antarmuka pengguna memberikan masukan kepada pengguna perangkat melalui cara komponen merespons interaksi pengguna. Setiap komponen memiliki cara sendiri untuk merespons interaksi, yang membantu pengguna mengetahui apa yang akan terjadi dengan interaksi yang mereka lakukan. Misalnya, jika pengguna menyentuh tombol pada layar sentuh perangkat, tombol tersebut kemungkinan akan berubah sedemikian rupa, mungkin dengan menambahkan warna sorotan. Perubahan ini memberi tahu pengguna bahwa mereka telah menyentuh tombol. Jika tidak ingin melakukannya, pengguna dapat menarik jarinya menjauh dari tombol sebelum melepaskannya. Jika tidak, tombol akan aktif.
![](https://developer.android.com/static/develop/ui/compose/images/touchinput/button-always-enabled.gif?authuser=6&hl=id)
![](https://developer.android.com/static/develop/ui/compose/images/touchinput/button-reflects-state.gif?authuser=6&hl=id)
Dokumentasi Gestur Compose mencakup cara komponen Compose menangani peristiwa pointer level rendah, seperti gerakan pointer dan klik. Secara langsung, Compose memisahkan peristiwa level rendah tersebut menjadi interaksi level yang lebih tinggi–misalnya, serangkaian peristiwa pointer dapat ditambahkan ke penekanan dan pelepasan tombol. Memahami abstraksi level tinggi tersebut dapat membantu Anda menyesuaikan respons UI terhadap pengguna. Misalnya, Anda mungkin ingin menyesuaikan perubahan tampilan komponen saat pengguna berinteraksi dengan komponen, atau mungkin Anda hanya ingin mempertahankan log tindakan pengguna tersebut. Dokumen ini memberikan informasi yang Anda perlukan untuk mengubah elemen UI standar, atau mendesain elemen UI Anda sendiri.
Interaksi
Dalam banyak kasus, Anda tidak perlu mengetahui cara komponen Compose
menafsirkan interaksi pengguna. Misalnya, Button
bergantung pada
Modifier.clickable
untuk mencari tahu apakah pengguna mengklik tombol atau tidak. Jika menambahkan tombol
khusus ke aplikasi, Anda dapat menentukan kode onClick
tombol, dan
Modifier.clickable
akan menjalankan kode tersebut jika sesuai. Itu berarti Anda tidak perlu
mengetahui apakah pengguna mengetuk layar atau memilih tombol dengan
keyboard; Modifier.clickable
mengetahui bahwa pengguna melakukan klik, dan
merespons dengan menjalankan kode onClick
.
Namun, jika ingin menyesuaikan respons komponen UI terhadap perilaku pengguna, Anda mungkin perlu mengetahui apa yang terjadi lebih lanjut. Bagian ini memberikan beberapa informasi tersebut.
Saat pengguna berinteraksi dengan komponen UI, sistem merepresentasikan perilakunya
dengan menghasilkan sejumlah
peristiwa
Interaction
. Misalnya, jika pengguna menyentuh tombol, tombol tersebut akan menghasilkan
PressInteraction.Press
.
Jika pengguna mengangkat jari di dalam tombol, tindakan ini akan menghasilkan
PressInteraction.Release
,
yang memberi tahu tombol bahwa klik telah selesai. Di sisi lain, jika
pengguna menarik jari ke luar tombol, lalu mengangkat jari, tombol
akan menghasilkan
PressInteraction.Cancel
,
untuk menunjukkan bahwa penekanan pada tombol dibatalkan, bukan diselesaikan.
Interaksi ini tidak terkonfigurasi. Yaitu, peristiwa interaksi level rendah ini tidak bermaksud menafsirkan makna tindakan pengguna, atau urutannya. Peristiwa ini juga tidak menafsirkan tindakan pengguna mana yang mungkin diprioritaskan dari tindakan lainnya.
Interaksi ini biasanya berpasangan, dengan awal dan akhir. Interaksi kedua
berisi referensi ke interaksi pertama. Misalnya, jika pengguna
menyentuh tombol, lalu mengangkat jarinya, sentuhan tersebut akan menghasilkan
interaksi
PressInteraction.Press
, dan pelepasan akan menghasilkan
PressInteraction.Release
;
Release
memiliki properti press
yang mengidentifikasi
PressInteraction.Press
awal.
Anda dapat melihat interaksi untuk komponen tertentu dengan mengamati
InteractionSource
-nya. InteractionSource
di-build di atas alur
Kotlin, sehingga Anda dapat mengumpulkan interaksi dari alur tersebut dengan cara yang sama
seperti Anda mengerjakan alur lainnya. Untuk informasi selengkapnya tentang keputusan desain ini,
lihat postingan blog Illuminating Interactions.
Status interaksi
Anda mungkin ingin memperluas fungsi bawaan komponen dengan
melacak interaksi sendiri. Misalnya, mungkin Anda ingin tombol
berubah warna saat ditekan. Cara termudah untuk melacak interaksi adalah dengan
mengamati status interaksi yang sesuai. InteractionSource
menawarkan sejumlah
metode yang mengungkap berbagai status interaksi sebagai status. Misalnya, jika
ingin melihat apakah tombol tertentu ditekan, Anda dapat memanggil
metode
InteractionSource.collectIsPressedAsState()
:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Selain collectIsPressedAsState()
, Compose juga menyediakan
collectIsFocusedAsState()
, collectIsDraggedAsState()
, dan
collectIsHoveredAsState()
. Metode ini sebenarnya adalah metode praktis
yang dibuat di atas InteractionSource
API dengan level yang lebih rendah. Dalam beberapa kasus, Anda mungkin
ingin menggunakan fungsi level rendah tersebut secara langsung.
Misalnya, anggaplah Anda perlu mengetahui apakah tombol sedang ditekan,
dan apakah tombol sedang ditarik. Jika Anda menggunakan collectIsPressedAsState()
dan collectIsDraggedAsState()
, Compose akan melakukan banyak tugas duplikat, dan
tidak ada jaminan Anda akan mendapatkan semua interaksi dalam urutan yang tepat. Untuk
situasi seperti ini, Anda mungkin ingin langsung menggunakan
InteractionSource
. Untuk informasi selengkapnya tentang melacak sendiri interaksi
dengan InteractionSource
, lihat Menggunakan InteractionSource
.
Bagian berikut menjelaskan cara menggunakan dan memunculkan interaksi dengan
InteractionSource
dan MutableInteractionSource
.
Memakai dan memunculkan Interaction
InteractionSource
mewakili aliran hanya baca Interactions
— tidak
dapat memunculkan Interaction
ke InteractionSource
. Untuk memunculkan
Interaction
, Anda harus menggunakan MutableInteractionSource
, yang diperluas dari
InteractionSource
.
Pengubah dan komponen dapat memakai, memunculkan, atau memakai dan memunculkan Interactions
.
Bagian berikut menjelaskan cara menggunakan dan memunculkan interaksi dari
pengubah dan komponen.
Contoh pengubah pemakaian
Untuk pengubah yang menggambar batas untuk status difokuskan, Anda hanya perlu mengamati
Interactions
, sehingga dapat menerima InteractionSource
:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
Dari tanda tangan fungsi, terlihat jelas bahwa pengubah ini adalah konsumen — pengubah ini
dapat menggunakan Interaction
, tetapi tidak dapat memunculkannya.
Contoh pengubah pembuatan
Untuk pengubah yang menangani peristiwa pengarahan kursor seperti Modifier.hoverable
, Anda
harus memunculkan Interactions
, dan menerima MutableInteractionSource
sebagai
parameter:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Pengubah ini adalah produser — dapat menggunakan MutableInteractionSource
yang disediakan untuk memunculkan HoverInteractions
saat kursor diarahkan atau
tidak diarahkan.
Membangun komponen yang mengkonsumsi dan menghasilkan
Komponen tingkat tinggi seperti Button
Material berfungsi sebagai produsen dan
konsumen. Fungsi ini menangani input dan peristiwa fokus, serta mengubah tampilannya
sebagai respons terhadap peristiwa ini, seperti menampilkan ripple atau menganimasikan
elevasinya. Akibatnya, fungsi ini secara langsung mengekspos MutableInteractionSource
sebagai
parameter, sehingga Anda dapat menyediakan instance yang diingat sendiri:
@Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, // exposes MutableInteractionSource as a parameter interactionSource: MutableInteractionSource? = null, elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { /* content() */ }
Hal ini memungkinkan pengangkatan
MutableInteractionSource
dari komponen dan mengamati semua
Interaction
yang dihasilkan oleh komponen. Anda dapat menggunakannya untuk mengontrol
tampilan komponen tersebut, atau komponen lain di UI.
Jika Anda membangun komponen tingkat tinggi interaktif, sebaiknya
tampilkan MutableInteractionSource
sebagai parameter dengan cara ini. Selain
mengikuti praktik terbaik pengangkatan status, hal ini juga memudahkan untuk membaca dan
mengontrol status visual komponen dengan cara yang sama seperti status
lainnya (seperti status diaktifkan) dapat dibaca dan dikontrol.
Compose mengikuti pendekatan arsitektur berlapis,
sehingga komponen Material tingkat tinggi dibuat di atas elemen penyusun dasar
yang menghasilkan Interaction
yang diperlukan untuk mengontrol ripple dan
efek visual lainnya. Library dasar menyediakan pengubah interaksi tingkat tinggi
seperti Modifier.hoverable
, Modifier.focusable
, dan
Modifier.draggable
.
Untuk membuat komponen yang merespons peristiwa pengarahan kursor, Anda cukup menggunakan
Modifier.hoverable
dan meneruskan MutableInteractionSource
sebagai parameter.
Setiap kali diarahkan, komponen akan memunculkan HoverInteraction
, dan Anda dapat menggunakannya
untuk mengubah tampilan komponen.
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Agar komponen ini juga dapat difokuskan, Anda dapat menambahkan Modifier.focusable
dan meneruskan
MutableInteractionSource
yang sama sebagai parameter. Sekarang, HoverInteraction.Enter/Exit
dan FocusInteraction.Focus/Unfocus
ditampilkan
melalui MutableInteractionSource
yang sama, dan Anda dapat menyesuaikan
penampilan untuk kedua jenis interaksi di tempat yang sama:
// This InteractionSource will emit hover and focus interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource) .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Modifier.clickable
adalah abstraksi
level yang lebih tinggi daripada hoverable
dan focusable
— agar komponen dapat
diklik, secara implisit dapat diarahkan, dan komponen yang dapat diklik juga
harus dapat difokuskan. Anda dapat menggunakan Modifier.clickable
untuk membuat komponen yang
menangani interaksi arahkan kursor, fokus, dan tekan, tanpa perlu menggabungkan API dengan
tingkat lebih rendah. Jika ingin membuat komponen juga dapat diklik, Anda dapat
mengganti hoverable
dan focusable
dengan clickable
:
// This InteractionSource will emit hover, focus, and press interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .clickable( onClick = {}, interactionSource = interactionSource, // Also show a ripple effect indication = ripple() ), contentAlignment = Alignment.Center ) { Text("Hello!") }
Bekerja dengan InteractionSource
Jika memerlukan informasi level rendah terkait interaksi dengan komponen, Anda dapat
menggunakan flow API standar untuk InteractionSource
komponen tersebut.
Misalnya, Anda ingin mempertahankan daftar interaksi tekan dan tarik
untuk InteractionSource
. Kode ini melakukan separuh pekerjaan, yang menambahkan
penekanan baru ke daftar saat masuk:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is DragInteraction.Start -> { interactions.add(interaction) } } } }
Namun, selain menambahkan interaksi baru, Anda juga harus menghapus interaksi saat interaksi tersebut berakhir (misalnya, saat pengguna mengangkat jarinya kembali dari komponen). Hal ini mudah dilakukan karena interaksi akhir selalu membawa referensi ke interaksi awal yang terkait. Kode ini menunjukkan cara menghapus interaksi yang telah berakhir:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } is DragInteraction.Start -> { interactions.add(interaction) } is DragInteraction.Stop -> { interactions.remove(interaction.start) } is DragInteraction.Cancel -> { interactions.remove(interaction.start) } } } }
Sekarang, jika ingin mengetahui apakah komponen saat ini ditekan atau ditarik,
yang harus Anda lakukan adalah memeriksa apakah interactions
kosong:
val isPressedOrDragged = interactions.isNotEmpty()
Jika Anda ingin tahu interaksi terbaru, cukup lihat item terakhir dalam daftar. Misalnya, ini adalah cara implementasi ripple Compose menentukan overlay status yang sesuai untuk digunakan untuk interaksi terbaru:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
Karena semua Interaction
mengikuti struktur yang sama, tidak ada banyak
perbedaan dalam kode saat menggunakan berbagai jenis interaksi pengguna — pola
keseluruhannya sama.
Perhatikan bahwa contoh sebelumnya di bagian ini mewakili Flow
interaksi menggunakan State
— hal ini memudahkan untuk mengamati nilai yang diperbarui,
karena membaca nilai status akan otomatis menyebabkan rekomposisi. Namun,
komposisi adalah pra-frame dikelompokkan. Ini berarti bahwa jika status berubah, lalu
berubah kembali dalam frame yang sama, komponen yang mengamati status tidak akan
melihat perubahan.
Hal ini penting untuk interaksi, karena interaksi dapat dimulai dan diakhiri secara teratur
dalam frame yang sama. Misalnya, menggunakan contoh sebelumnya dengan Button
:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Jika penekanan dimulai dan berakhir dalam frame yang sama, teks tidak akan ditampilkan sebagai
"Pressed!". Pada umumnya, hal ini bukanlah masalah. Menampilkan efek visual dalam
waktu yang singkat akan menyebabkan layar berkedip, dan tidak akan terlalu
terlihat bagi pengguna. Untuk beberapa kasus, seperti menampilkan efek ripple atau
animasi serupa, sebaiknya Anda menampilkan efek setidaknya untuk jangka waktu minimum, bukan langsung berhenti jika tombol tidak lagi ditekan. Untuk
melakukannya, Anda dapat langsung memulai dan menghentikan animasi dari dalam lambda
mengumpulkan, bukan menulis ke status. Terdapat contoh pola ini di
bagian Membangun Indication
lanjutan dengan batas animasi.
Contoh: Mem-build komponen dengan penanganan interaksi kustom
Untuk mengetahui cara mem-build komponen dengan respons kustom terhadap input, berikut contoh tombol yang dimodifikasi. Dalam hal ini, misalnya Anda menginginkan tombol yang merespons penekanan dengan mengubah tampilannya:
![Animasi tombol yang secara dinamis menambahkan ikon keranjang belanja saat diklik](https://developer.android.com/static/develop/ui/compose/images/handling-interactions-iconbutton.gif?authuser=6&hl=id)
Untuk melakukannya, build composable kustom berdasarkan Button
, dan minta parameter
icon
tambahan untuk menggambar ikon (dalam hal ini, keranjang belanja). Anda
memanggil collectIsPressedAsState()
untuk melacak apakah pengguna mengarahkan kursor ke
tombol; saat itu terjadi, Anda menambahkan ikon. Kode akan terlihat seperti berikut ini:
@Composable fun PressIconButton( onClick: () -> Unit, icon: @Composable () -> Unit, text: @Composable () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource? = null ) { val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false Button( onClick = onClick, modifier = modifier, interactionSource = interactionSource ) { AnimatedVisibility(visible = isPressed) { if (isPressed) { Row { icon() Spacer(Modifier.size(ButtonDefaults.IconSpacing)) } } } text() } }
Dan berikut tampilan penggunaan composable baru tersebut:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
Karena PressIconButton
baru ini di-build di atas Button
Material yang ada, kode ini bereaksi terhadap interaksi pengguna dengan cara yang biasa. Saat pengguna menekannya, tombol akan sedikit mengubah opasitasnya, seperti Button
Material biasa.
Membuat dan menerapkan efek kustom yang dapat digunakan kembali dengan Indication
Di bagian sebelumnya, Anda telah mempelajari cara mengubah bagian komponen sebagai respons
terhadap Interaction
yang berbeda, seperti menampilkan ikon saat ditekan. Pendekatan yang sama
ini dapat digunakan untuk mengubah nilai parameter yang Anda berikan pada
komponen, atau mengubah konten yang ditampilkan di dalam komponen, tetapi ini
hanya berlaku untuk setiap komponen. Sering kali, aplikasi atau sistem desain
akan memiliki sistem generik untuk efek visual stateful — efek yang harus
diterapkan ke semua komponen secara konsisten.
Jika Anda mem-build sistem desain semacam ini, menyesuaikan satu komponen dan menggunakan kembali penyesuaian ini untuk komponen lain bisa jadi sulit karena alasan berikut:
- Setiap komponen dalam sistem desain memerlukan boilerplate yang sama
- Sangat mudah untuk lupa menerapkan efek ini ke komponen yang baru dibuat dan komponen kustom yang dapat diklik
- Mungkin sulit untuk menggabungkan efek kustom dengan efek lain
Untuk menghindari masalah ini dan menskalakan komponen kustom dengan mudah di seluruh sistem,
Anda dapat menggunakan Indication
.
Indication
merepresentasikan efek visual yang dapat digunakan kembali yang dapat diterapkan di seluruh
komponen dalam aplikasi atau sistem desain. Indication
dibagi menjadi dua
bagian:
IndicationNodeFactory
: Factory yang membuat instanceModifier.Node
yang merender efek visual untuk komponen. Untuk implementasi yang lebih sederhana dan tidak berubah di seluruh komponen, implementasi ini dapat berupa singleton (objek) dan digunakan kembali di seluruh aplikasi.Instance ini bisa stateful atau stateless. Karena dibuat per komponen, nilai tersebut dapat mengambil nilai dari
CompositionLocal
untuk mengubah cara tampilan atau perilakunya dalam komponen tertentu, seperti denganModifier.Node
lainnya.Modifier.indication
: Pengubah yang menggambarIndication
untuk komponen.Modifier.clickable
dan pengubah interaksi tingkat tinggi lainnya menerima parameter indikasi secara langsung, sehingga tidak hanya memunculkanInteraction
, tetapi juga dapat menggambar efek visual untukInteraction
yang dikeluarkan. Jadi, untuk kasus sederhana, Anda cukup menggunakanModifier.clickable
tanpa memerlukanModifier.indication
.
Ganti efek dengan Indication
Bagian ini menjelaskan cara mengganti efek skala manual yang diterapkan ke satu tombol tertentu dengan indikasi setara yang dapat digunakan kembali di beberapa komponen.
Kode berikut membuat tombol yang diskalakan ke bawah saat ditekan:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") Button( modifier = Modifier.scale(scale), onClick = { }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Untuk mengonversi efek skala dalam cuplikan di atas menjadi Indication
, ikuti
langkah-langkah berikut:
Buat
Modifier.Node
yang bertanggung jawab untuk menerapkan efek skala. Jika dipasang, node mengamati sumber interaksi, mirip dengan contoh sebelumnya. Satu-satunya perbedaan di sini adalah tindakan ini langsung meluncurkan animasi, bukan mengonversi Interaksi masuk menjadi status.Node perlu mengimplementasikan
DrawModifierNode
agar dapat menggantiContentDrawScope#draw()
, dan merender efek skala menggunakan perintah gambar yang sama seperti API grafis lainnya di Compose.Memanggil
drawContent()
yang tersedia dari penerimaContentDrawScope
akan menggambar komponen sebenarnya tempatIndication
harus diterapkan, sehingga Anda hanya perlu memanggil fungsi ini dalam transformasi skala. Pastikan implementasiIndication
Anda selalu memanggildrawContent()
pada waktu tertentu; jika tidak, komponen tempat Anda menerapkanIndication
tidak akan digambar.private class ScaleNode(private val interactionSource: InteractionSource) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedScalePercent = Animatable(1f) private suspend fun animateToPressed(pressPosition: Offset) { currentPressPosition = pressPosition animatedScalePercent.animateTo(0.9f, spring()) } private suspend fun animateToResting() { animatedScalePercent.animateTo(1f, spring()) } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collectLatest { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { scale( scale = animatedScalePercent.value, pivot = currentPressPosition ) { this@draw.drawContent() } } }
Buat
IndicationNodeFactory
. Tanggung jawab satu-satunya adalah membuat instance node baru untuk sumber interaksi yang disediakan. Karena tidak ada parameter untuk mengonfigurasi indikasi, factory dapat berupa objek:object ScaleIndication : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ScaleNode(interactionSource) } override fun equals(other: Any?): Boolean = other === ScaleIndication override fun hashCode() = 100 }
Modifier.clickable
menggunakanModifier.indication
secara internal, jadi untuk membuat komponen yang dapat diklik denganScaleIndication
, Anda hanya perlu menyediakanIndication
sebagai parameter untukclickable
:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
Hal ini juga memudahkan Anda mem-build komponen tingkat tinggi yang dapat digunakan kembali menggunakan
Indication
kustom — tombol akan terlihat seperti ini:@Composable fun ScaleButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, shape: Shape = CircleShape, content: @Composable RowScope.() -> Unit ) { Row( modifier = modifier .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) .clickable( enabled = enabled, indication = ScaleIndication, interactionSource = interactionSource, onClick = onClick ) .border(width = 2.dp, color = Color.Blue, shape = shape) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) }
Kemudian, Anda dapat menggunakan tombol tersebut dengan cara berikut:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
![Animasi tombol dengan ikon keranjang belanja yang mengecil saat ditekan](https://developer.android.com/static/develop/ui/compose/images/touchinput/button-custom-indication.gif?authuser=6&hl=id)
Indication
kustom.Membangun Indication
lanjutan dengan batas animasi
Indication
tidak hanya terbatas pada efek transformasi, seperti menskalakan
komponen. Karena IndicationNodeFactory
menampilkan Modifier.Node
, Anda dapat menggambar
segala jenis efek di atas atau di bawah konten seperti API gambar lainnya. Misalnya, Anda dapat menggambar batas animasi di sekitar komponen dan overlay di
atas komponen saat ditekan:
![Tombol dengan efek pelangi elegan saat ditekan](https://developer.android.com/static/develop/ui/compose/images/touchinput/rainbow-effect.gif?authuser=6&hl=id)
Indication
.Implementasi Indication
di sini sangat mirip dengan contoh sebelumnya —
hanya membuat node dengan beberapa parameter. Karena batas animasi bergantung
pada bentuk dan batas komponen yang digunakan Indication
, implementasi
Indication
juga memerlukan bentuk dan lebar batas untuk disediakan
sebagai parameter:
data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return NeonNode( shape, // Double the border size for a stronger press effect borderWidth * 2, interactionSource ) } }
Implementasi Modifier.Node
juga secara konseptual sama, meskipun
kode gambar lebih rumit. Seperti sebelumnya, library ini mengamati InteractionSource
saat dilampirkan, meluncurkan animasi, dan mengimplementasikan DrawModifierNode
untuk menggambar
efek di atas konten:
private class NeonNode( private val shape: Shape, private val borderWidth: Dp, private val interactionSource: InteractionSource ) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedProgress = Animatable(0f) val animatedPressAlpha = Animatable(1f) var pressedAnimation: Job? = null var restingAnimation: Job? = null private suspend fun animateToPressed(pressPosition: Offset) { // Finish any existing animations, in case of a new press while we are still showing // an animation for a previous one restingAnimation?.cancel() pressedAnimation?.cancel() pressedAnimation = coroutineScope.launch { currentPressPosition = pressPosition animatedPressAlpha.snapTo(1f) animatedProgress.snapTo(0f) animatedProgress.animateTo(1f, tween(450)) } } private fun animateToResting() { restingAnimation = coroutineScope.launch { // Wait for the existing press animation to finish if it is still ongoing pressedAnimation?.join() animatedPressAlpha.animateTo(0f, tween(250)) animatedProgress.snapTo(0f) } } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( currentPressPosition, size ) val brush = animateBrush( startPosition = startPosition, endPosition = endPosition, progress = animatedProgress.value ) val alpha = animatedPressAlpha.value drawContent() val outline = shape.createOutline(size, layoutDirection, this) // Draw overlay on top of content drawOutline( outline = outline, brush = brush, alpha = alpha * 0.1f ) // Draw border on top of overlay drawOutline( outline = outline, brush = brush, alpha = alpha, style = Stroke(width = borderWidth.toPx()) ) } /** * Calculates a gradient start / end where start is the point on the bounding rectangle of * size [size] that intercepts with the line drawn from the center to [pressPosition], * and end is the intercept on the opposite end of that line. */ private fun calculateGradientStartAndEndFromPressPosition( pressPosition: Offset, size: Size ): Pair<Offset, Offset> { // Convert to offset from the center val offset = pressPosition - size.center // y = mx + c, c is 0, so just test for x and y to see where the intercept is val gradient = offset.y / offset.x // We are starting from the center, so halve the width and height - convert the sign // to match the offset val width = (size.width / 2f) * sign(offset.x) val height = (size.height / 2f) * sign(offset.y) val x = height / gradient val y = gradient * width // Figure out which intercept lies within bounds val intercept = if (abs(y) <= abs(height)) { Offset(width, y) } else { Offset(x, height) } // Convert back to offsets from 0,0 val start = intercept + size.center val end = Offset(size.width - start.x, size.height - start.y) return start to end } private fun animateBrush( startPosition: Offset, endPosition: Offset, progress: Float ): Brush { if (progress == 0f) return TransparentBrush // This is *expensive* - we are doing a lot of allocations on each animation frame. To // recreate a similar effect in a performant way, it would be better to create one large // gradient and translate it on each frame, instead of creating a whole new gradient // and shader. The current approach will be janky! val colorStops = buildList { when { progress < 1 / 6f -> { val adjustedProgress = progress * 6f add(0f to Blue) add(adjustedProgress to Color.Transparent) } progress < 2 / 6f -> { val adjustedProgress = (progress - 1 / 6f) * 6f add(0f to Purple) add(adjustedProgress * MaxBlueStop to Blue) add(adjustedProgress to Blue) add(1f to Color.Transparent) } progress < 3 / 6f -> { val adjustedProgress = (progress - 2 / 6f) * 6f add(0f to Pink) add(adjustedProgress * MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 4 / 6f -> { val adjustedProgress = (progress - 3 / 6f) * 6f add(0f to Orange) add(adjustedProgress * MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 5 / 6f -> { val adjustedProgress = (progress - 4 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } else -> { val adjustedProgress = (progress - 5 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxYellowStop to Yellow) add(MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } } } return linearGradient( colorStops = colorStops.toTypedArray(), start = startPosition, end = endPosition ) } companion object { val TransparentBrush = SolidColor(Color.Transparent) val Blue = Color(0xFF30C0D8) val Purple = Color(0xFF7848A8) val Pink = Color(0xFFF03078) val Orange = Color(0xFFF07800) val Yellow = Color(0xFFF0D800) const val MaxYellowStop = 0.16f const val MaxOrangeStop = 0.33f const val MaxPinkStop = 0.5f const val MaxPurpleStop = 0.67f const val MaxBlueStop = 0.83f } }
Perbedaan utamanya di sini adalah sekarang ada durasi minimum untuk
animasi dengan fungsi animateToResting()
, jadi meskipun penekanan
segera dirilis, animasi pers akan berlanjut. Ada juga penanganan
untuk beberapa penekanan cepat di awal animateToPressed
— jika penekanan
terjadi selama animasi tekan atau istirahat yang ada, animasi sebelumnya akan
dibatalkan, dan animasi pers dimulai dari awal. Untuk mendukung beberapa
efek serentak (seperti dengan ripple, dengan animasi ripple baru akan digambar
di atas ripple lainnya), Anda dapat melacak animasi dalam daftar, bukan
membatalkan animasi yang ada dan memulai animasi baru.
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Memahami gestur
- Kotlin untuk Jetpack Compose
- Komponen Material dan tata letak