Compose menyediakan banyak pengubah untuk perilaku umum yang langsung dapat digunakan, tetapi Anda juga dapat membuat pengubah kustom Anda sendiri.
Pengubah memiliki beberapa bagian:
- Factory pengubah
- Ini adalah fungsi ekstensi di
Modifier
yang menyediakan API idiomatis untuk pengubah dan memungkinkan pengubah dengan mudah dirantai bersama. Tujuan factory pengubah menghasilkan elemen pengubah yang digunakan oleh Compose untuk mengubah UI Anda.
- Ini adalah fungsi ekstensi di
- Elemen pengubah
- Di sinilah Anda dapat menerapkan perilaku pengubah.
Ada beberapa cara untuk menerapkan pengubah kustom bergantung pada
fungsionalitas yang dibutuhkan. Sering kali, cara termudah untuk menerapkan pengubah kustom adalah
untuk menerapkan factory pengubah kustom yang
menggabungkan filter lain yang sudah ditentukan
pabrik pengubah bersama-sama. Jika Anda memerlukan perilaku kustom lainnya, implementasikan
elemen pengubah menggunakan Modifier.Node
API, yang merupakan level lebih rendah tetapi
memberikan fleksibilitas yang lebih besar.
Gabungkan pengubah yang ada
Membuat pengubah kustom sering kali hanya dengan menggunakan pengubah yang ada
pengubah. Misalnya, Modifier.clip()
diimplementasikan menggunakan
Pengubah graphicsLayer
. Strategi ini menggunakan elemen pengubah yang ada, dan Anda
menyediakan factory pengubah kustom Anda sendiri.
Sebelum menerapkan pengubah kustom Anda sendiri, lihat apakah Anda dapat menggunakan strategi.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
Atau, jika Anda sering mengulangi grup pengubah yang sama, Anda dapat gabungkan ke dalam pengubah Anda sendiri:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Membuat pengubah kustom menggunakan factory pengubah composable
Anda juga dapat membuat pengubah kustom menggunakan fungsi composable untuk meneruskan nilai ke pengubah yang ada. Ini dikenal sebagai factory pengubah composable.
Menggunakan factory pengubah composable untuk membuat pengubah juga memungkinkan penggunaan
API compose dengan level yang lebih tinggi, seperti animate*AsState
dan Compose lainnya
API animasi yang didukung status. Misalnya, cuplikan berikut menampilkan
pengubah yang menganimasikan perubahan alfa saat diaktifkan/dinonaktifkan:
@Composable fun Modifier.fade(enable: Boolean): Modifier { val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f) return this then Modifier.graphicsLayer { this.alpha = alpha } }
Jika pengubah kustom merupakan metode praktis untuk memberikan nilai default dari
CompositionLocal
, cara termudah untuk menerapkannya adalah dengan menggunakan composable
factory pengubah:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
Pendekatan ini memiliki beberapa peringatan yang dijelaskan di bawah.
Nilai CompositionLocal
diselesaikan di situs panggilan factory pengubah
Saat membuat pengubah kustom menggunakan factory pengubah composable, penduduk setempat mengambil nilai dari pohon komposisi tempat mereka dibuat, bukan data Hal ini dapat memberikan hasil yang tidak diharapkan. Misalnya, pada komposisi contoh pengubah lokal dari atas, diterapkan sedikit berbeda dengan menggunakan fungsi composable:
@Composable fun Modifier.myBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) } @Composable fun MyScreen() { CompositionLocalProvider(LocalContentColor provides Color.Green) { // Background modifier created with green background val backgroundModifier = Modifier.myBackground() // LocalContentColor updated to red CompositionLocalProvider(LocalContentColor provides Color.Red) { // Box will have green background, not red as expected. Box(modifier = backgroundModifier) } } }
Jika pengubah ini tidak berfungsi sebagaimana yang Anda harapkan, gunakan
Modifier.Node
, karena lokal komposisi akan
diselesaikan dengan benar di lokasi penggunaan
dan dapat diangkat dengan aman.
Pengubah fungsi composable tidak pernah dilewati
Pengubah factory composable tidak pernah dilewati karena fungsi composable yang memiliki nilai hasil tidak dapat dilewati. Ini berarti fungsi pengubah akan dipanggil di setiap rekomposisi, yang mungkin mahal jika merekomposisi secara rutin.
Pengubah fungsi composable harus dipanggil dalam fungsi composable
Seperti semua fungsi composable, pengubah factory composable harus dipanggil dari dalam komposisi. Ini membatasi tempat pengubah dapat diangkat, karena tidak pernah diangkat dari komposisi. Sebagai perbandingan, pengubah non-composable {i>factory<i} dapat diangkat dari fungsi composable untuk memungkinkan penggunaan kembali meningkatkan kinerja:
val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations @Composable fun Modifier.composableModifier(): Modifier { val color = LocalContentColor.current.copy(alpha = 0.5f) return this then Modifier.background(color) } @Composable fun MyComposable() { val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher }
Menerapkan perilaku pengubah kustom menggunakan Modifier.Node
Modifier.Node
adalah API level yang lebih rendah untuk membuat pengubah di Compose. Ini
adalah API yang sama dengan tempat Compose menerapkan pengubahnya sendiri dan merupakan
yang lebih andal untuk membuat pengubah kustom.
Terapkan pengubah kustom menggunakan Modifier.Node
Ada tiga bagian dalam menerapkan pengubah kustom menggunakan Modifier.Node:
- Implementasi
Modifier.Node
yang menyimpan logika dan status pengubah Anda. ModifierNodeElement
yang membuat dan memperbarui pengubah instance node.- Factory pengubah opsional seperti dijelaskan di atas.
Class ModifierNodeElement
bersifat stateless dan instance baru dialokasikan masing-masing
rekomposisi, sedangkan class Modifier.Node
bisa stateful dan akan bertahan
di beberapa rekomposisi, dan bahkan
dapat digunakan kembali.
Bagian berikut menjelaskan setiap bagian dan menunjukkan contoh pembuatan pengubah kustom untuk menggambar lingkaran.
Modifier.Node
Implementasi Modifier.Node
(dalam contoh ini, CircleNode
) menerapkan
tindakan
fungsi pengubah kustom Anda.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
Dalam contoh ini, menggambar lingkaran dengan warna yang diteruskan ke pengubah .
Node mengimplementasikan Modifier.Node
serta nol atau beberapa jenis node. Ada
berbagai jenis node berdasarkan fungsi
yang diperlukan pengubah Anda. Tujuan
contoh di atas harus dapat menggambar, sehingga mengimplementasikan DrawModifierNode
, yang
memungkinkannya untuk mengganti metode menggambar.
Jenis yang tersedia adalah sebagai berikut:
Node |
Penggunaan |
Contoh Link |
|
||
|
||
Menerapkan antarmuka ini memungkinkan |
||
|
||
|
||
|
||
|
||
|
||
|
||
Hal ini berguna untuk menggabungkan beberapa implementasi node menjadi satu. |
||
Mengizinkan class |
Node akan otomatis menjadi tidak valid saat update dipanggil di node yang sesuai
. Karena contoh kita adalah DrawModifierNode
, setiap update akan dipanggil di
elemen, node memicu penggambaran ulang dan warnanya diperbarui dengan benar. Penting
Anda dapat memilih untuk tidak ikut serta dalam pembatalan validasi otomatis sebagaimana dijelaskan di bawah.
ModifierNodeElement
ModifierNodeElement
adalah class yang tidak dapat diubah dan menyimpan data yang akan dibuat atau
perbarui pengubah kustom Anda:
// ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } }
Implementasi ModifierNodeElement
harus mengganti metode berikut:
create
: Ini adalah fungsi yang membuat instance node pengubah. Hal ini dipanggil untuk membuat node saat pengubah Anda pertama kali diterapkan. Biasanya, ini sama seperti membuat node dan mengkonfigurasinya dengan parameter yang diteruskan ke factory pengubah.update
: Fungsi ini dipanggil setiap kali pengubah ini diberikan dalam tempat yang sama dengan simpul ini, tetapi properti telah berubah. Ini adalah ditentukan oleh metode classequals
. Simpul pengubah yang yang dibuat sebelumnya akan dikirim sebagai parameter ke panggilanupdate
. Pada tahap ini, Anda harus memperbarui node properti untuk sesuai dengan parameter. Kemampuan {i>node<i} untuk digunakan kembali dengan cara ini adalah kunci untuk peningkatan performa yang dihasilkanModifier.Node
; Oleh karena itu, Anda harus memperbarui node yang sudah ada, bukan membuat node baru di metodeupdate
. Di lingkaran, warna simpul akan diperbarui.
Selain itu, implementasi ModifierNodeElement
juga harus mengimplementasikan equals
dan hashCode
. update
hanya akan dipanggil jika perbandingan yang sama dengan
elemen sebelumnya mengembalikan
nilai salah (false).
Contoh di atas menggunakan class data untuk mencapai ini. Metode ini digunakan untuk
memeriksa apakah {i>node<i} perlu diupdate atau tidak. Jika elemen Anda memiliki
properti yang
tidak berkontribusi pada apakah {i>node<i} perlu diperbarui, atau Anda ingin menghindari
karena alasan kompatibilitas biner, maka Anda dapat menerapkan equals
secara manual
dan hashCode
mis. elemen pengubah padding.
Factory pengubah
Ini adalah platform API publik pengubah Anda. Kebanyakan implementasi hanya buat elemen pengubah dan tambahkan ke rantai pengubah:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Contoh lengkap
Ketiga bagian ini bersatu untuk membuat pengubah kustom guna menggambar lingkaran
menggunakan Modifier.Node
API:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color) // ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } } // Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
Situasi umum saat menggunakan Modifier.Node
Saat membuat pengubah kustom dengan Modifier.Node
, berikut adalah beberapa situasi umum yang mungkin Anda
hadapi.
Tidak ada parameter
Jika pengubah tidak memiliki parameter, pengubah tidak perlu melakukan update dan, selanjutnya, tidak perlu berupa class data. Berikut adalah contoh implementasi pengubah yang menerapkan padding dalam jumlah tetap ke composable:
fun Modifier.fixedPadding() = this then FixedPaddingElement data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() { override fun create() = FixedPaddingNode() override fun update(node: FixedPaddingNode) {} } class FixedPaddingNode : LayoutModifierNode, Modifier.Node() { private val PADDING = 16.dp override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val paddingPx = PADDING.roundToPx() val horizontal = paddingPx * 2 val vertical = paddingPx * 2 val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) val width = constraints.constrainWidth(placeable.width + horizontal) val height = constraints.constrainHeight(placeable.height + vertical) return layout(width, height) { placeable.place(paddingPx, paddingPx) } } }
Mereferensikan lokal komposisi
Pengubah Modifier.Node
tidak otomatis mengamati perubahan pada status Compose
seperti CompositionLocal
. Keuntungan yang dimiliki pengubah Modifier.Node
lebih dari
pengubah yang baru saja dibuat dengan factory composable adalah dapat membaca
nilai lokal komposisi tempat pengubah digunakan di UI Anda
hierarki, bukan tempat pengubah dialokasikan, menggunakan currentValueOf
.
Namun, instance node pengubah tidak secara otomatis mengamati perubahan status. Kepada secara otomatis bereaksi terhadap perubahan lokal komposisi, Anda dapat membaca nilai di dalam cakupan:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
&IntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
Contoh ini mengamati nilai LocalContentColor
untuk menggambar berbasis latar belakang
pada warnanya. Karena ContentDrawScope
mengamati perubahan snapshot,
otomatis menggambar ulang saat nilai LocalContentColor
berubah:
class BackgroundColorConsumerNode : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode { override fun ContentDrawScope.draw() { val currentColor = currentValueOf(LocalContentColor) drawRect(color = currentColor) drawContent() } }
Untuk bereaksi terhadap perubahan status di luar cakupan dan memperbarui secara otomatis
pengubah, gunakan ObserverModifierNode
.
Misalnya, Modifier.scrollable
menggunakan teknik ini untuk
mengamati perubahan di LocalDensity
. Contoh yang disederhanakan ditampilkan di bawah ini:
class ScrollableNode : Modifier.Node(), ObserverModifierNode, CompositionLocalConsumerModifierNode { // Place holder fling behavior, we'll initialize it when the density is available. val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity)) override fun onAttach() { updateDefaultFlingBehavior() observeReads { currentValueOf(LocalDensity) } // monitor change in Density } override fun onObservedReadsChanged() { // if density changes, update the default fling behavior. updateDefaultFlingBehavior() } private fun updateDefaultFlingBehavior() { val density = currentValueOf(LocalDensity) defaultFlingBehavior.flingDecay = splineBasedDecay(density) } }
Menganimasikan pengubah
Implementasi Modifier.Node
memiliki akses ke coroutineScope
. Hal ini memungkinkan
penggunaan Compose Animatable API. Misalnya, cuplikan ini memodifikasi
CircleNode
dari atas untuk memperjelas dan memudar berulang kali:
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private val alpha = Animatable(1f) override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { coroutineScope.launch { alpha.animateTo( 0f, infiniteRepeatable(tween(1000), RepeatMode.Reverse) ) { } } } }
Berbagi status antar-pengubah menggunakan delegasi
Pengubah Modifier.Node
dapat didelegasikan ke node lain. Ada banyak kasus penggunaan
untuk ini, seperti mengekstrak implementasi
umum di berbagai pengubah,
tetapi juga dapat digunakan untuk berbagi status umum di seluruh pengubah.
Misalnya, implementasi dasar dari node pengubah yang dapat diklik yang membagikan data interaksi:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Memilih tidak ikut pembatalan validasi otomatis node
Modifier.Node
node secara otomatis dibatalkan saat node yang sesuai
ModifierNodeElement
panggilan diperbarui. Terkadang, dalam pengubah yang
lebih kompleks, Anda mungkin
ingin memilih tidak ikut serta dalam hal ini untuk memiliki
kontrol yang lebih terperinci saat
pengubah membatalkan fase.
Hal ini dapat sangat berguna jika pengubah kustom Anda mengubah tata letak dan
menggambar. Memilih tidak ikut pembatalan otomatis memungkinkan Anda membatalkan penarikan saat
hanya properti terkait gambar, seperti color
, yang mengubah, dan tidak membatalkan tata letak.
Tindakan ini dapat meningkatkan performa pengubah.
Contoh hipotesis ini ditunjukkan di bawah dengan pengubah yang memiliki color
,
size
, dan lambda onClick
sebagai properti. Pengubah ini hanya membatalkan
diperlukan, dan mengabaikan pembatalan validasi yang tidak:
class SampleInvalidatingNode( var color: Color, var size: IntSize, var onClick: () -> Unit ) : DelegatingNode(), LayoutModifierNode, DrawModifierNode { override val shouldAutoInvalidate: Boolean get() = false private val clickableNode = delegate( ClickablePointerInputNode(onClick) ) fun update(color: Color, size: IntSize, onClick: () -> Unit) { if (this.color != color) { this.color = color // Only invalidate draw when color changes invalidateDraw() } if (this.size != size) { this.size = size // Only invalidate layout when size changes invalidateMeasurement() } // If only onClick changes, we don't need to invalidate anything clickableNode.update(onClick) } override fun ContentDrawScope.draw() { drawRect(color) } override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val size = constraints.constrain(size) val placeable = measurable.measure(constraints) return layout(size.width, size.height) { placeable.place(0, 0) } } }