Bermigrasi ke Indication dan Ripple API

Untuk meningkatkan performa komposisi komponen interaktif yang menggunakan Modifier.clickable, kami telah memperkenalkan API baru. API ini memungkinkan penerapan Indication yang lebih efisien, seperti riak.

androidx.compose.foundation:foundation:1.7.0+ dan androidx.compose.material:material-ripple:1.7.0+ mencakup perubahan API berikut:

Tidak digunakan lagi

Penggantian

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

API ripple() baru disediakan di library Material.

Catatan: Dalam konteks ini, "library Material" mengacu pada androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material, dan androidx.wear.compose:compose-material3.

RippleTheme

Yakni:

  • Gunakan API RippleConfiguration library Material, atau
  • Membangun implementasi riak sistem desain Anda sendiri

Halaman ini menjelaskan dampak perubahan perilaku dan petunjuk untuk bermigrasi ke API baru.

Perubahan perilaku

Versi library berikut menyertakan perubahan perilaku riak:

  • androidx.compose.material:material:1.7.0+
  • androidx.compose.material3:material3:1.3.0+
  • androidx.wear.compose:compose-material:1.4.0+

Versi library Material ini tidak lagi menggunakan rememberRipple(); sebagai gantinya, library ini menggunakan API riak baru. Akibatnya, mereka tidak mengkueri LocalRippleTheme. Oleh karena itu, jika Anda menyetel LocalRippleTheme di aplikasi, komponen Material tidak akan menggunakan nilai ini.

Bagian berikut menjelaskan cara bermigrasi ke API baru.

Migrasi dari rememberRipple ke ripple

Menggunakan library Material

Jika Anda menggunakan library Material, ganti rememberRipple() secara langsung dengan panggilan ke ripple() dari library yang sesuai. API ini membuat riak menggunakan nilai yang berasal dari API tema Material. Kemudian, teruskan objek yang ditampilkan ke Modifier.clickable dan/atau komponen lainnya.

Misalnya, cuplikan berikut menggunakan API yang tidak digunakan lagi:

Box(
    Modifier.clickable(
        onClick = {},
        interactionSource = remember { MutableInteractionSource() },
        indication = rememberRipple()
    )
) {
    // ...
}

Anda harus mengubah cuplikan di atas menjadi:

@Composable
private fun RippleExample() {
    Box(
        Modifier.clickable(
            onClick = {},
            interactionSource = remember { MutableInteractionSource() },
            indication = ripple()
        )
    ) {
        // ...
    }
}

Perhatikan bahwa ripple() bukan lagi fungsi composable dan tidak perlu diingat. Ripple juga dapat digunakan kembali di beberapa komponen, mirip dengan pengubah, jadi pertimbangkan untuk mengekstrak pembuatan ripple ke nilai tingkat teratas untuk menghemat alokasi.

Menerapkan sistem desain kustom

Jika Anda menerapkan sistem desain sendiri, dan sebelumnya menggunakan rememberRipple() bersama dengan RippleTheme kustom untuk mengonfigurasi riak, sebaiknya berikan API riak Anda sendiri yang mendelegasikan ke API node riak yang diekspos di material-ripple. Kemudian, komponen Anda dapat menggunakan ripple Anda sendiri yang menggunakan nilai tema Anda secara langsung. Untuk mengetahui informasi selengkapnya, lihat Bermigrasi dariRippleTheme.

Migrasi dari RippleTheme

Menggunakan RippleTheme untuk menonaktifkan riak untuk komponen tertentu

Library material dan material3 mengekspos RippleConfiguration dan LocalRippleConfiguration, yang memungkinkan Anda mengonfigurasi tampilan efek riak dalam subtree. Perhatikan bahwa RippleConfiguration dan LocalRippleConfiguration bersifat eksperimental, dan hanya ditujukan untuk penyesuaian per komponen. Penyesuaian global/di seluruh tema tidak didukung dengan API ini; lihat Menggunakan RippleTheme untuk mengubah semua riak secara global dalam aplikasi untuk mengetahui informasi selengkapnya tentang kasus penggunaan tersebut.

Misalnya, cuplikan berikut menggunakan API yang tidak digunakan lagi:

private object DisabledRippleTheme : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Transparent

    @Composable
    override fun rippleAlpha(): RippleAlpha = RippleAlpha(0f, 0f, 0f, 0f)
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleTheme) {
        Button {
            // ...
        }
    }

Anda harus mengubah cuplikan di atas menjadi:

CompositionLocalProvider(LocalRippleConfiguration provides null) {
    Button {
        // ...
    }
}

Menggunakan RippleTheme untuk mengubah warna/alfa riak untuk komponen tertentu

Seperti yang dijelaskan di bagian sebelumnya, RippleConfiguration dan LocalRippleConfiguration adalah API eksperimental dan hanya ditujukan untuk penyesuaian per komponen.

Misalnya, cuplikan berikut menggunakan API yang tidak digunakan lagi:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Red

    @Composable
    override fun rippleAlpha(): RippleAlpha = MyRippleAlpha
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleThemeColorAndAlpha) {
        Button {
            // ...
        }
    }

Anda harus mengubah cuplikan di atas menjadi:

@OptIn(ExperimentalMaterialApi::class)
private val MyRippleConfiguration =
    RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha)

// ...
    CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) {
        Button {
            // ...
        }
    }

Menggunakan RippleTheme untuk mengubah semua riak dalam aplikasi secara global

Sebelumnya, Anda dapat menggunakan LocalRippleTheme untuk menentukan perilaku riak di tingkat seluruh tema. Pada dasarnya, ini adalah titik integrasi antara lokal komposisi sistem desain kustom dan riak. Daripada menampilkan primitif tema generik, material-ripple kini menampilkan fungsi createRippleModifierNode(). Fungsi ini memungkinkan library sistem desain membuat implementasi wrapper urutan yang lebih tinggi, yang membuat kueri nilai tema mereka, lalu mendelegasikan implementasi riak ke node yang dibuat oleh fungsi ini.

Hal ini memungkinkan sistem desain untuk langsung membuat kueri yang mereka butuhkan, dan mengekspos lapisan tema yang dapat dikonfigurasi pengguna yang diperlukan di atas tanpa harus sesuai dengan apa yang disediakan di lapisan material-ripple. Perubahan ini juga membuat tema/spesifikasi yang dipatuhi riak menjadi lebih eksplisit, karena API riak itu sendiri yang menentukan kontrak tersebut, bukan diturunkan secara implisit dari tema.

Untuk panduan, lihat penerapan API riak di library Material, dan ganti panggilan ke lokal komposisi Material sesuai kebutuhan untuk sistem desain Anda sendiri.

Migrasi dari Indication ke IndicationNodeFactory

Melewati Indication

Jika Anda hanya membuat Indication untuk dibagikan, seperti membuat ripl untuk dibagikan ke Modifier.clickable atau Modifier.indication, Anda tidak perlu melakukan perubahan apa pun. IndicationNodeFactory diwarisi dari Indication, sehingga semuanya akan terus dikompilasi dan berfungsi.

Membuat Indication

Jika Anda membuat implementasi Indication sendiri, migrasi seharusnya sederhana dalam sebagian besar kasus. Misalnya, pertimbangkan Indication yang menerapkan efek penskalaan saat ditekan:

object ScaleIndication : Indication {
    @Composable
    override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
        // key the remember against interactionSource, so if it changes we create a new instance
        val instance = remember(interactionSource) { ScaleIndicationInstance() }

        LaunchedEffect(interactionSource) {
            interactionSource.interactions.collectLatest { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                    is PressInteraction.Release -> instance.animateToResting()
                    is PressInteraction.Cancel -> instance.animateToResting()
                }
            }
        }

        return instance
    }
}

private class ScaleIndicationInstance : IndicationInstance {
    var currentPressPosition: Offset = Offset.Zero
    val animatedScalePercent = Animatable(1f)

    suspend fun animateToPressed(pressPosition: Offset) {
        currentPressPosition = pressPosition
        animatedScalePercent.animateTo(0.9f, spring())
    }

    suspend fun animateToResting() {
        animatedScalePercent.animateTo(1f, spring())
    }

    override fun ContentDrawScope.drawIndication() {
        scale(
            scale = animatedScalePercent.value,
            pivot = currentPressPosition
        ) {
            this@drawIndication.drawContent()
        }
    }
}

Anda dapat memigrasikan ini dalam dua langkah:

  1. Migrasikan ScaleIndicationInstance menjadi DrawModifierNode. Permukaan API untuk DrawModifierNode sangat mirip dengan IndicationInstance: API ini mengekspos fungsi ContentDrawScope#draw() yang secara fungsional setara dengan IndicationInstance#drawContent(). Anda perlu mengubah fungsi tersebut, lalu menerapkan logika collectLatest langsung di dalam node, bukan Indication.

    Misalnya, cuplikan berikut menggunakan API yang tidak digunakan lagi:

    private class ScaleIndicationInstance : IndicationInstance {
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun ContentDrawScope.drawIndication() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@drawIndication.drawContent()
            }
        }
    }

    Anda harus mengubah cuplikan di atas menjadi:

    private class ScaleIndicationNode(
        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()
            }
        }
    }

  2. Migrasikan ScaleIndication untuk menerapkan IndicationNodeFactory. Karena logika pengumpulan kini dipindahkan ke node, ini adalah objek factory yang sangat sederhana yang hanya bertanggung jawab untuk membuat instance node.

    Misalnya, cuplikan berikut menggunakan API yang tidak digunakan lagi:

    object ScaleIndication : Indication {
        @Composable
        override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
            // key the remember against interactionSource, so if it changes we create a new instance
            val instance = remember(interactionSource) { ScaleIndicationInstance() }
    
            LaunchedEffect(interactionSource) {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> instance.animateToResting()
                        is PressInteraction.Cancel -> instance.animateToResting()
                    }
                }
            }
    
            return instance
        }
    }

    Anda harus mengubah cuplikan di atas menjadi:

    object ScaleIndicationNodeFactory : IndicationNodeFactory {
        override fun create(interactionSource: InteractionSource): DelegatableNode {
            return ScaleIndicationNode(interactionSource)
        }
    
        override fun hashCode(): Int = -1
    
        override fun equals(other: Any?) = other === this
    }

Menggunakan Indication untuk membuat IndicationInstance

Dalam sebagian besar kasus, Anda harus menggunakan Modifier.indication untuk menampilkan Indication untuk komponen. Namun, jika Anda membuat IndicationInstance secara manual menggunakan rememberUpdatedInstance, Anda harus memperbarui penerapan untuk memeriksa apakah Indication adalah IndicationNodeFactory sehingga Anda dapat menggunakan penerapan yang lebih ringan. Misalnya, Modifier.indication akan mendelegasikan secara internal ke node yang dibuat jika merupakan IndicationNodeFactory. Jika tidak, Modifier.composed akan digunakan untuk memanggil rememberUpdatedInstance.