Membuat pengubah kustom

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.
  • 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

LayoutModifierNode

Modifier.Node yang mengubah cara pengukuran dan penataan konten yang digabungkan.

Contoh

DrawModifierNode

Modifier.Node yang menggambar ke ruang tata letak.

Contoh

CompositionLocalConsumerModifierNode

Menerapkan antarmuka ini memungkinkan Modifier.Node untuk membaca lokal komposisi.

Contoh

SemanticsModifierNode

Modifier.Node yang menambahkan kunci/nilai semantik untuk digunakan dalam pengujian, aksesibilitas, dan kasus penggunaan serupa.

Contoh

PointerInputModifierNode

Modifier.Node yang menerima PointerInputChanges.

Contoh

ParentDataModifierNode

Modifier.Node yang menyediakan data ke tata letak induk.

Contoh

LayoutAwareModifierNode

Modifier.Node yang menerima callback onMeasured dan onPlaced.

Contoh

GlobalPositionAwareModifierNode

Modifier.Node yang menerima callback onGloballyPositioned dengan LayoutCoordinates akhir tata letak saat posisi global konten mungkin telah berubah.

Contoh

ObserverModifierNode

Modifier.Node yang mengimplementasikan ObserverNode dapat memberikan implementasi onObservedReadsChanged-nya sendiri yang akan dipanggil sebagai respons terhadap perubahan pada objek snapshot yang dibaca dalam blok observeReads.

Contoh

DelegatingNode

Modifier.Node yang dapat mendelegasikan pekerjaan ke instance Modifier.Node lain.

Hal ini berguna untuk menggabungkan beberapa implementasi node menjadi satu.

Contoh

TraversableNode

Mengizinkan class Modifier.Node melewati hierarki node ke atas/bawah untuk class dari jenis yang sama atau untuk kunci tertentu.

Contoh

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:

  1. 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.
  2. 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 class equals. Simpul pengubah yang yang dibuat sebelumnya akan dikirim sebagai parameter ke panggilan update. 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 dihasilkan Modifier.Node; Oleh karena itu, Anda harus memperbarui node yang sudah ada, bukan membuat node baru di metode update. 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:

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)
        }
    }
}