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 melakukannya, mereka akan menarik jari mereka dari tombol sebelum melepaskan--jika tidak, tombol akan berfungsi.
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 mengetahui informasi selengkapnya tentang keputusan desain ini, lihat postingan blog Interaksi yang Lebih Jelas.
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 mengetahui informasi selengkapnya tentang cara melacak interaksi sendiri dengan InteractionSource, lihat Bekerja dengan InteractionSource.
Bagian berikut menjelaskan cara menggunakan dan memancarkan interaksi dengan
InteractionSource dan MutableInteractionSource.
Menggunakan dan memancarkan Interaction
InteractionSource mewakili aliran hanya baca dari Interactions — Interaction tidak dapat
dipancarkan ke InteractionSource. Untuk memancarkan
Interaction, Anda perlu menggunakan MutableInteractionSource, yang diperluas dari
InteractionSource.
Pengubah dan komponen dapat menggunakan, memancarkan, atau menggunakan dan memancarkan Interactions.
Bagian berikut menjelaskan cara menggunakan dan memancarkan interaksi dari pengubah dan komponen.
Contoh pengubah penggunaan
Untuk pengubah yang menggambar batas untuk status fokus, Anda hanya perlu mengamati
Interactions, sehingga Anda dapat menerima InteractionSource:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
Dari tanda tangan fungsi, jelas bahwa pengubah ini adalah konsumen — pengubah ini
dapat menggunakan Interactions, tetapi tidak dapat memancarkannya.
Contoh pengubah produksi
Untuk pengubah yang menangani peristiwa arahkan kursor seperti Modifier.hoverable, Anda
perlu memancarkan Interactions, dan menerima MutableInteractionSource sebagai
parameter:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Pengubah ini adalah produser — dapat menggunakan
MutableInteractionSource yang disediakan untuk memancarkan HoverInteractions saat kursor diarahkan atau
tidak diarahkan.
Membangun komponen yang menggunakan dan menghasilkan
Komponen tingkat tinggi seperti Button Material bertindak sebagai produsen dan
konsumen. Komponen ini menangani peristiwa input dan fokus, serta mengubah tampilannya
sebagai respons terhadap peristiwa ini, seperti menampilkan riak atau menganimasikan
ketinggiannya. Akibatnya, MutableInteractionSource diekspos secara langsung sebagai parameter, sehingga Anda dapat memberikan 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 dalam UI Anda.
Jika Anda membuat komponen tingkat tinggi interaktif Anda sendiri, sebaiknya
tampilkan MutableInteractionSource sebagai parameter dengan cara ini. Selain
mengikuti praktik terbaik penarikan status, hal ini juga memudahkan pembacaan dan
pengontrolan status visual komponen dengan cara yang sama seperti jenis
status lainnya (seperti status aktif) dapat dibaca dan dikontrol.
Compose mengikuti pendekatan arsitektur berlayer,
sehingga komponen Material tingkat tinggi dibangun di atas blok
dasar yang menghasilkan Interaction yang diperlukan untuk mengontrol riak dan efek
visual lainnya. Library fondasi 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 kursor diarahkan ke komponen, komponen akan memancarkan 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!") }
Untuk membuat 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 dipancarkan 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 adalah abstraksi tingkat yang lebih tinggi
daripada hoverable dan focusable — agar komponen dapat
diklik, komponen tersebut secara implisit dapat di-hover, 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 tingkat yang lebih rendah. Jika Anda juga ingin membuat komponen 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 mengetahui interaksi terbaru, cukup lihat item terakhir dalam daftar. Misalnya, ini adalah cara implementasi ripple Compose mengetahui overlay status yang sesuai untuk digunakan dalam 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 bekerja dengan berbagai jenis interaksi pengguna — pola keseluruhannya sama.
Perhatikan bahwa contoh sebelumnya di bagian ini merepresentasikan Flow interaksi menggunakan State
— hal ini memudahkan pengamatan nilai yang diperbarui,
karena membaca nilai status akan otomatis menyebabkan rekomposisi. Namun,
komposisi dikelompokkan sebelum frame. Artinya, jika status berubah, lalu
berubah kembali dalam frame yang sama, komponen yang mengamati status tidak akan
melihat perubahan tersebut.
Hal ini penting untuk interaksi, karena interaksi dapat dimulai dan diakhiri secara rutin
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 pernah ditampilkan sebagai
"Ditekan!". Dalam kebanyakan kasus, hal ini bukan masalah — menampilkan efek visual dalam waktu yang sangat singkat akan menyebabkan kedipan, dan tidak akan terlalu terlihat oleh pengguna. Untuk beberapa kasus, seperti menampilkan efek riak atau animasi serupa, Anda mungkin ingin menampilkan efek selama durasi minimum, bukan langsung berhenti jika tombol tidak lagi ditekan. Untuk
melakukannya, Anda dapat langsung memulai dan menghentikan animasi dari dalam lambda pengumpulan, bukan menulis ke status. Ada contoh pola ini di bagian Membangun Indication lanjutan dengan batas animasi.
Contoh: Membangun 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:
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 dapat digunakan untuk mengubah nilai parameter yang Anda berikan ke
komponen, atau mengubah konten yang ditampilkan di dalam komponen, tetapi hal ini
hanya berlaku per 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 membuat sistem desain semacam ini, menyesuaikan satu komponen dan menggunakan kembali penyesuaian ini untuk komponen lain dapat menjadi sulit karena alasan berikut:
- Setiap komponen dalam sistem desain memerlukan boilerplate yang sama
- Efek ini sering lupa diterapkan ke komponen yang baru dibuat dan komponen yang dapat diklik kustom
- Efek kustom mungkin sulit digabungkan 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 dan diterapkan di seluruh
komponen dalam aplikasi atau sistem desain. Indication dibagi menjadi dua bagian:
IndicationNodeFactory: Factory yang membuat instanceModifier.Nodeyang merender efek visual untuk komponen. Untuk penerapan yang lebih sederhana yang tidak berubah di seluruh komponen, ini dapat berupa singleton (objek) dan digunakan kembali di seluruh aplikasi.Instance ini dapat berupa stateful atau stateless. Karena dibuat per komponen, mereka dapat mengambil nilai dari
CompositionLocaluntuk mengubah tampilan atau perilakunya di dalam komponen tertentu, sepertiModifier.Nodelainnya.Modifier.indication: Pengubah yang menggambarIndicationuntuk komponen.Modifier.clickabledan pengubah interaksi tingkat tinggi lainnya menerima parameter indikasi secara langsung, sehingga tidak hanya memancarkanInteraction, tetapi juga dapat menggambar efek visual untukInteractionyang dipancarkannya. Jadi, untuk kasus sederhana, Anda cukup menggunakanModifier.clickabletanpa memerlukanModifier.indication.
Mengganti efek dengan Indication
Bagian ini menjelaskan cara mengganti efek skala manual yang diterapkan pada satu tombol tertentu dengan indikasi setara yang dapat digunakan kembali di beberapa komponen.
Kode berikut membuat tombol yang mengecil 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.Nodeyang bertanggung jawab untuk menerapkan efek skala. Saat dilampirkan, node mengamati sumber interaksi, mirip dengan contoh sebelumnya. Satu-satunya perbedaan di sini adalah bahwa animasi diluncurkan secara langsung, bukan mengonversi Interaksi yang masuk ke status.Node perlu menerapkan
DrawModifierNodesehingga dapat menggantiContentDrawScope#draw(), dan merender efek skala menggunakan perintah penggambaran yang sama seperti dengan API grafis lainnya di Compose.Memanggil
drawContent()yang tersedia dari penerimaContentDrawScopeakan menggambar komponen sebenarnya yang harus diterapkanIndication, jadi Anda hanya perlu memanggil fungsi ini dalam transformasi skala. Pastikan penerapanIndicationAnda selalu memanggildrawContent()di beberapa titik; jika tidak, komponen yang Anda terapkanIndicationtidak 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 membuat instance node baru untuk sumber interaksi yang diberikan. 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.clickablemenggunakanModifier.indicationsecara internal, jadi untuk membuat komponen yang dapat diklik denganScaleIndication, yang perlu Anda lakukan adalah memberikanIndicationsebagai parameter keclickable: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
Indicationkustom — 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 dengan cara berikut:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
Indication kustom.Membangun Indication lanjutan dengan batas animasi
Indication tidak hanya terbatas pada efek transformasi, seperti penskalaan
komponen. Karena IndicationNodeFactory menampilkan Modifier.Node, Anda dapat menggambar
efek apa pun di atas atau di bawah konten seperti dengan API gambar lainnya. Misalnya, Anda dapat menggambar batas animasi di sekitar komponen dan overlay di
atas komponen saat ditekan:
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, penerapan Indication juga memerlukan bentuk dan lebar batas yang diberikan 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
penggambarannya lebih rumit. Seperti sebelumnya, fungsi ini mengamati InteractionSource
saat dilampirkan, meluncurkan animasi, dan menerapkan 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 adalah kini ada durasi minimum untuk animasi dengan fungsi animateToResting(), jadi meskipun tekanan segera dilepaskan, animasi tekanan akan berlanjut. Ada juga penanganan untuk beberapa penekanan cepat di awal animateToPressed — jika penekanan terjadi selama penekanan yang ada atau animasi istirahat, animasi sebelumnya dibatalkan, dan animasi penekanan dimulai dari awal. Untuk mendukung beberapa efek serentak (seperti dengan riak, di mana animasi riak baru akan digambar di atas riak lainnya), Anda dapat melacak animasi dalam daftar, alih-alih membatalkan animasi yang ada dan memulai yang baru.
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Memahami gestur
- Kotlin untuk Jetpack Compose
- Komponen Material dan tata letak