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 pengguna tidak ingin melakukan mereka akan menyeret jari mereka menjauh dari tombol sebelum melepaskan--jika tidak, tombol akan aktif.
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 pelacakan interaksi
diri Anda sendiri dengan InteractionSource
, lihat Bekerja dengan InteractionSource
.
Bagian berikut menjelaskan cara memakai dan memunculkan interaksi dengan
InteractionSource
dan MutableInteractionSource
.
Menggunakan dan mengeluarkan Interaction
InteractionSource
mewakili aliran hanya baca Interactions
— bukan
mungkin memunculkan Interaction
ke InteractionSource
. Untuk memancarkan
Interaction
, Anda perlu menggunakan MutableInteractionSource
, yang diperluas dari
InteractionSource
.
Pengubah dan komponen dapat menggunakan, memunculkan, atau memakai dan memunculkan Interactions
.
Bagian berikut menjelaskan cara memakai dan memunculkan interaksi dari keduanya
pengubah dan komponen.
Memakai contoh pengubah
Untuk pengubah yang menggambar batas untuk status fokus, Anda hanya perlu mengamati
Interactions
, sehingga Anda dapat menyetujui InteractionSource
:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
Dari tanda tangan fungsi, terlihat jelas bahwa pengubah ini adalah konsumen, yaitu pengubah
dapat memakai Interaction
, tetapi tidak dapat memunculkannya.
Membuat contoh pengubah
Untuk pengubah yang menangani peristiwa pengarahan kursor seperti Modifier.hoverable
, Anda
perlu memunculkan Interactions
, dan menerima MutableInteractionSource
sebagai
parameter sebagai gantinya:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Pengubah ini adalah produser — Pengubah ini dapat menggunakan
MutableInteractionSource
untuk memunculkan HoverInteractions
saat kursor diarahkan ke atasnya atau
tidak diarahkan kursor.
Membangun komponen yang menggunakan dan menghasilkan
Komponen tingkat tinggi seperti Button
Material berfungsi sebagai produser dan
konsumen. Mereka menangani input dan memfokuskan peristiwa, serta mengubah penampilan mereka
sebagai respons terhadap peristiwa ini, seperti menampilkan ripple atau menganimasikan
elevasi. Akibatnya, fungsi tersebut secara langsung mengekspos MutableInteractionSource
sebagai
, sehingga Anda dapat memberikan instance yang Anda ingat 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 Anda.
Jika Anda membuat komponen interaktif tingkat tinggi, sebaiknya
Anda mengekspos MutableInteractionSource
sebagai parameter dengan cara ini. Selain
mengikuti praktik terbaik pengangkatan status, hal ini juga membuatnya mudah dibaca dan
mengontrol keadaan visual komponen dengan
cara yang sama seperti
(seperti status aktif) dapat dibaca dan dikontrol.
Compose mengikuti pendekatan arsitektur berlapis,
sehingga komponen Material tingkat tinggi dibangun di atas bangunan dasar
blok yang menghasilkan Interaction
yang mereka butuhkan untuk mengontrol ripple dan
efek visual. Library dasar menyediakan pengubah interaksi tingkat tinggi
seperti Modifier.hoverable
, Modifier.focusable
, dan
Modifier.draggable
.
Untuk membangun komponen yang merespons peristiwa pengarahan kursor, Anda cukup menggunakan
Modifier.hoverable
dan teruskan MutableInteractionSource
sebagai parameter.
Setiap kali kursor diarahkan ke atasnya, komponen akan memunculkan HoverInteraction
, dan Anda dapat menggunakan
ini 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 dengan parameter. Sekarang, keduanya
HoverInteraction.Enter/Exit
dan FocusInteraction.Focus/Unfocus
dimunculkan
melalui MutableInteractionSource
yang sama, dan Anda dapat menyesuaikan
tampilan 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
jauh lebih tinggi
abstraksi tingkat daripada hoverable
dan focusable
— agar komponen
dapat diklik, secara implisit dapat diarahkan, dan komponen yang dapat diklik harus
juga dapat difokuskan. Anda dapat menggunakan Modifier.clickable
untuk membuat komponen yang
menangani pengarahan kursor, fokus, dan interaksi tekan, tanpa perlu menggabungkan elemen
level API. Jika Anda juga ingin membuat
komponen yang dapat diklik, Anda dapat
ganti 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 apa interaksi terbaru, lihat saja interaksi item baris dalam daftar. Misalnya, ini adalah cara implementasi ripple Compose mencari tahu overlay status yang sesuai untuk digunakan bagi 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 banyak
perbedaan kode saat bekerja dengan berbagai jenis interaksi pengguna —
pola keseluruhannya sama.
Perhatikan bahwa contoh sebelumnya di bagian ini mewakili Flow
interaksi menggunakan State
— hal ini memudahkan kita untuk mengamati nilai yang telah diperbarui,
karena membaca nilai status akan otomatis menyebabkan rekomposisi. Namun,
komposisi adalah batch pre-frame. Ini berarti bahwa jika
status berubah, dan
kemudian berubah kembali dalam {i>frame<i} yang sama, komponen yang mengamati status tidak
melihat perubahannya.
Hal ini penting untuk interaksi, karena interaksi dapat dimulai dan diakhiri secara rutin
dalam {i>frame<i} 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 {i>frame<i} yang sama, teks tidak akan pernah ditampilkan sebagai
"Ditekan!". Di kebanyakan kasus, ini bukan masalah — menunjukkan efek visual untuk
dalam waktu singkat akan menyebabkan
kedipan, dan tidak terlalu
terlihat jelas oleh pengguna. Untuk beberapa kasus, seperti menampilkan efek riak atau
animasi serupa, sebaiknya tampilkan efek setidaknya dengan jumlah minimum
waktu, alih-alih langsung berhenti
jika tombol tidak lagi ditekan. Kepada
melakukannya, Anda dapat langsung memulai dan
menghentikan animasi dari dalam
lambda, alih-alih menulis ke status. Ada contoh dari pola ini
bagian Membangun Indication
lanjutan dengan batas animasi.
Contoh: Komponen build 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:
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 sebagian komponen sebagai respons
ke Interaction
yang berbeda, seperti menampilkan ikon saat ditekan. Hal yang sama
dapat digunakan untuk mengubah nilai parameter yang Anda berikan ke
komponen, atau mengubah konten yang ditampilkan di dalam komponen, namun ini
hanya berlaku per komponen. Sering kali, aplikasi atau sistem desain
akan memiliki sistem generik untuk efek visual stateful — sebuah efek yang seharusnya
diterapkan ke semua komponen dengan cara yang konsisten.
Jika Anda membangun sistem desain semacam ini, menyesuaikan satu komponen dan menggunakan kembali penyesuaian ini untuk komponen lain bisa sulit bagi alasan berikut:
- Setiap komponen dalam sistem desain membutuhkan boilerplate yang sama
- Sangat mudah untuk lupa menerapkan efek ini ke komponen yang baru dibuat dan komponen yang dapat diklik
- Mungkin sulit untuk menggabungkan efek kustom dengan efek lainnya
Untuk menghindari masalah ini dan menskalakan komponen kustom dengan mudah di seluruh sistem Anda,
Anda dapat menggunakan Indication
.
Indication
mewakili efek visual yang dapat digunakan kembali yang dapat diterapkan
komponen dalam suatu aplikasi
atau sistem desain. Indication
dibagi menjadi dua
suku cadang:
IndicationNodeFactory
: Factory yang membuat instanceModifier.Node
yang merender efek visual untuk sebuah komponen. Untuk implementasi yang lebih sederhana yang tidak berubah di seluruh komponen, ini bisa berupa singleton (objek) dan digunakan kembali di seluruh aplikasi.Instance ini dapat bersifat stateful atau stateless. Karena mereka dibuat berdasarkan , mereka dapat mengambil nilai dari
CompositionLocal
untuk mengubah cara elemen itu muncul atau berperilaku di dalam komponen tertentu, seperti yang lainnyaModifier.Node
.Modifier.indication
: Pengubah yang menggambarIndication
untuk komponen.Modifier.clickable
dan pengubah interaksi tingkat tinggi lainnya menerima parameter indikasi secara langsung, sehingga mereka tidak hanyaInteraction
, tetapi juga dapat menggambar efek visual untukInteraction
yang yang dihasilkan. Jadi, untuk kasus sederhana, Anda cukup menggunakanModifier.clickable
tanpa membutuhkanModifier.indication
.
Ganti efek dengan Indication
Bagian ini menjelaskan cara mengganti efek skala manual yang diterapkan ke efek tombol tertentu dengan setara tanda yang dapat digunakan kembali di berbagai 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. Saat terpasang, node mengamati sumber interaksi, mirip dengan yang sebelumnya contoh. Satu-satunya perbedaan di sini adalah ia langsung meluncurkan animasi alih-alih mengonversi Interaksi yang masuk menjadi status.Node perlu mengimplementasikan
DrawModifierNode
agar dapat menggantiContentDrawScope#draw()
, dan render efek skala menggunakan gambar yang sama seperti halnya API grafis lainnya di Compose.Memanggil
drawContent()
yang tersedia dari penerimaContentDrawScope
akan diambil komponen aktual tempatIndication
harus diterapkan, sehingga Anda memanggil fungsi ini di dalam transformasi skala. Pastikan atribut ImplementasiIndication
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
. Satu-satunya tanggung jawabnya adalah untuk membuat instance node baru untuk sumber interaksi yang disediakan. Karena tidak ada parameter untuk mengonfigurasi indikasi, factory bisa 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
, yang perlu Anda lakukan adalah 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 pembuatan komponen tingkat tinggi yang dapat digunakan kembali menggunakan
Indication
— tombol dapat terlihat seperti:@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!") }
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 pada API gambar lainnya. Sebagai
misalnya, Anda dapat menggambar {i>border<i} animasi di sekitar komponen dan {i>overlay<i} di
bagian atas komponen ketika ditekan:
Implementasi Indication
di sini sangat mirip dengan contoh sebelumnya —
alat itu hanya membuat {i>
node<i} dengan beberapa parameter. Karena {i>border<i} animasi tergantung
pada bentuk dan batas komponen yang digunakan Indication
,
Penerapan Indication
juga memerlukan penyediaan bentuk dan lebar batas
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
menggambar kode
menjadi lebih rumit. Seperti sebelumnya, modul 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 utama di sini adalah bahwa sekarang
ada durasi minimum untuk
animasi dengan fungsi animateToResting()
, sehingga meskipun penekanannya
segera dirilis, animasi pers akan dilanjutkan. Terdapat juga penanganan
untuk beberapa penekanan cepat di awal animateToPressed
— jika penekanan
terjadi selama penekanan atau animasi istirahat yang ada, animasi sebelumnya akan
dibatalkan, dan animasi pers
dimulai dari awal. Untuk mendukung banyak
efek serentak (seperti dengan ripple, tempat animasi ripple baru akan menggambar
di atas ripple lainnya), Anda dapat melacak animasi dalam daftar, alih-alih
membatalkan animasi yang sudah 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