Bermigrasi ke Indication dan Ripple API

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

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

Sebagai gantinya, 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 ripple 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 ripple:

  • 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 ripple baru. Akibatnya, mereka tidak mengkueri LocalRippleTheme. Oleh karena itu, jika Anda menetapkan LocalRippleTheme di aplikasi, komponen Material tidak akan menggunakan nilai ini.

Bagian berikut menjelaskan cara bermigrasi ke API baru.

Bermigrasi 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 ripple menggunakan nilai yang berasal dari API Penerapan 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 atas untuk menghemat alokasi.

Menerapkan sistem desain kustom

Jika Anda menerapkan sistem desain sendiri, dan sebelumnya menggunakan rememberRipple() bersama dengan RippleTheme kustom untuk mengonfigurasi ripple, Anda harus menyediakan API ripple Anda sendiri yang mendelegasikan ke API node ripple 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 ripple untuk komponen tertentu

Library material dan material3 mengekspos RippleConfiguration dan LocalRippleConfiguration, yang memungkinkan Anda mengonfigurasi tampilan ripple dalam subhierarki. Perhatikan bahwa RippleConfiguration dan LocalRippleConfiguration hanya ditujukan untuk penyesuaian per komponen. Penyesuaian global/seluruh tema tidak didukung dengan API ini; lihat Menggunakan RippleTheme untuk mengubah semua ripple secara global dalam aplikasi untuk 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 ripple untuk komponen tertentu

Seperti yang dijelaskan di bagian sebelumnya, RippleConfiguration dan LocalRippleConfiguration 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 ripple dalam aplikasi secara global

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

Hal ini memungkinkan sistem desain untuk langsung mengkueri apa yang mereka butuhkan, dan mengekspos lapisan tema yang dapat dikonfigurasi pengguna yang diperlukan di atasnya tanpa harus menyesuaikan dengan apa yang disediakan di lapisan material-ripple. Perubahan ini juga memperjelas tema/spesifikasi yang diikuti oleh ripple, karena API ripple itu sendiri yang menentukan kontrak tersebut, bukan secara implisit berasal dari tema.

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

Bermigrasi dari Indication ke IndicationNodeFactory

Melewati sekitar Indication

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

Membuat Indication

Jika Anda membuat implementasi Indication sendiri, migrasi akan mudah dilakukan dalam sebagian besar kasus. Misalnya, pertimbangkan Indication yang menerapkan efek skala 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 memigrasikannya dalam dua langkah:

  1. Migrasikan ScaleIndicationInstance menjadi DrawModifierNode. Platform API untuk DrawModifierNode sangat mirip dengan IndicationInstance: platform ini mengekspos fungsi ContentDrawScope#draw() yang secara fungsional setara dengan IndicationInstance#drawContent(). Anda perlu mengubah fungsi tersebut, lalu mengimplementasikan logika collectLatest di dalam node secara langsung, 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 mengimplementasikan IndicationNodeFactory. Karena logika pengumpulan kini dipindahkan ke node, ini adalah objek factory yang sangat sederhana yang satu-satunya tanggung jawabnya adalah 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

Pada umumnya, Anda harus menggunakan Modifier.indication untuk menampilkan Indication untuk komponen. Namun, dalam kasus yang jarang terjadi saat Anda membuat IndicationInstance secara manual menggunakan rememberUpdatedInstance, Anda perlu mengupdate implementasi untuk memeriksa apakah Indication adalah IndicationNodeFactory sehingga Anda dapat menggunakan implementasi yang lebih ringan. Misalnya, Modifier.indication akan mendelegasikan secara internal ke node yang dibuat jika node tersebut adalah IndicationNodeFactory. Jika tidak, aplikasi akan menggunakan Modifier.composed untuk memanggil rememberUpdatedInstance.