Переход на Indication и Ripple API

Чтобы повысить производительность интерактивных компонентов, в которых используется тег Modifier.clickable, мы добавили новые API. Эти API позволяют более эффективно реализовывать Indication, например создавать эффекты волны.

androidx.compose.foundation:foundation:1.7.0+ и androidx.compose.material:material-ripple:1.7.0+ включают следующие изменения API:

Столбец больше не доступен.

Замена

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

Новые API ripple() теперь доступны в библиотеках Material.

Примечание. В этом контексте под библиотеками материалов подразумеваются androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material и androidx.wear.compose:compose-material3..

RippleTheme

Выполните одно из следующих действий:

  • использовать API библиотеки Material RippleConfiguration;
  • Как создать собственную реализацию эффекта волны в системе проектирования

На этой странице описаны последствия изменения и приведены инструкции по переходу на новые API.

Изменение поведения

В следующих версиях библиотеки изменено поведение эффекта "Волна":

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

В этих версиях библиотек Material больше не используется rememberRipple(). Вместо него применяются новые API эффекта пульсации. Поэтому они не отправляют запросы к LocalRippleTheme. Поэтому, если вы зададите значение LocalRippleTheme в приложении, компоненты Material не будут использовать эти значения.

В следующих разделах описано, как перейти на новые API.

Как перейти с rememberRipple на ripple

Как использовать библиотеку материалов

Если вы используете библиотеку Material, замените rememberRipple() на вызов ripple() из соответствующей библиотеки. Этот API создает эффект волны, используя значения, полученные от API тем Material. Затем передайте возвращенный объект в Modifier.clickable и/или другие компоненты.

Например, в следующем фрагменте кода используются устаревшие API:

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

Измените приведенный выше фрагмент кода следующим образом:

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

Обратите внимание, что ripple() больше не является компонуемой функцией и запоминать ее не нужно. Его также можно использовать повторно в нескольких компонентах, как и модификаторы, поэтому, чтобы сэкономить ресурсы, попробуйте извлечь создание эффекта в значение верхнего уровня.

Как реализовать собственную систему проектирования

Если вы реализуете собственную систему дизайна и ранее использовали rememberRipple() вместе с пользовательским RippleTheme для настройки эффекта пульсации, вам следует предоставить собственный API пульсации, который делегирует API узла пульсации, представленные в material-ripple. Затем ваши компоненты могут использовать собственную волну, которая напрямую потребляет значения темы. Дополнительную информацию можно найти в статье Переход с RippleTheme.

Перенос из RippleTheme

Как отключить эффект пульсации для определенного компонента с помощью RippleTheme

Библиотеки material и material3 предоставляют доступ к классам RippleConfiguration и LocalRippleConfiguration, которые позволяют настраивать внешний вид эффекта ряби в поддереве. Обратите внимание, что параметры RippleConfiguration и LocalRippleConfiguration предназначены только для настройки отдельных компонентов. Эти API не поддерживают глобальную настройку или настройку на уровне темы. Подробнее о том, как использовать RippleTheme для глобального изменения всех эффектов пульсации в приложении, рассказывается в другом разделе.

Например, в следующем фрагменте кода используются устаревшие API:

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 {
            // ...
        }
    }

Измените приведенный выше фрагмент кода следующим образом:

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

Как использовать RippleTheme, чтобы изменить цвет или альфа-канал эффекта пульсации для определенного компонента

Как описано в предыдущем разделе, параметры RippleConfiguration и LocalRippleConfiguration предназначены только для настройки отдельных компонентов.

Например, в следующем фрагменте кода используются устаревшие API:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

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

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

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

Измените приведенный выше фрагмент кода следующим образом:

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

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

Использование RippleTheme для глобального изменения всех эффектов в приложении.

Ранее с помощью LocalRippleTheme можно было задать поведение эффекта волны на уровне всей темы. Это точка интеграции между локальными композициями пользовательской системы дизайна и эффектом пульсации. Вместо того чтобы использовать примитив для создания тем, material-ripple теперь предоставляет функцию createRippleModifierNode(). Эта функция позволяет библиотекам систем дизайна создавать реализацию более высокого порядка wrapper, которая запрашивает значения темы, а затем делегирует реализацию эффекта пульсации узлу, созданному этой функцией.

Это позволяет системам дизайна напрямую запрашивать нужные им данные и накладывать на них необходимые пользовательские слои тем, не ориентируясь на то, что предоставляется на уровне material-ripple. Кроме того, теперь более явно указывается, какой теме или спецификации соответствует эффект, поскольку это определяется самим API эффекта, а не неявно выводится из темы.

Инструкции по реализации API эффекта пульсации приведены в библиотеках Material. Замените вызовы локальных композиций Material в соответствии с требованиями вашей дизайн-системы.

Как перейти с Indication на IndicationNodeFactory

Проезжая мимо Indication

Если вы просто создаете Indication, чтобы передать его, например, создаете волну, чтобы передать ее в Modifier.clickable или Modifier.indication, вам не нужно ничего менять. IndicationNodeFactory наследует от Indication, поэтому все будет компилироваться и работать.

Создание Indication

Если вы создаете собственную реализацию Indication, в большинстве случаев переход будет простым. Например, рассмотрим Indication, в котором при нажатии применяется эффект масштабирования:

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

Перенести данные можно в два этапа:

  1. Перенесите ScaleIndicationInstance в DrawModifierNode. API для DrawModifierNode очень похож на API для IndicationInstance: он предоставляет функцию ContentDrawScope#draw(), которая функционально эквивалентна IndicationInstance#drawContent(). Вам нужно изменить эту функцию, а затем реализовать логику collectLatest непосредственно в узле, а не в Indication.

    Например, в следующем фрагменте кода используются устаревшие API:

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

    Измените приведенный выше фрагмент кода следующим образом:

    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. Перенесите проекты из ScaleIndication, чтобы реализовать IndicationNodeFactory. Поскольку логика коллекции теперь перенесена в узел, это очень простой фабричный объект, единственная задача которого – создать экземпляр узла.

    Например, в следующем фрагменте кода используются устаревшие API:

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

    Измените приведенный выше фрагмент кода следующим образом:

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

Как создать IndicationInstance с помощью Indication

В большинстве случаев для отображения Indication для компонента следует использовать Modifier.indication. Однако в редких случаях, когда вы вручную создаете IndicationInstance с помощью rememberUpdatedInstance, вам нужно обновить реализацию, чтобы проверить, является ли Indication IndicationNodeFactory. Это позволит вам использовать более простую реализацию. Например, если Modifier.indication является IndicationNodeFactory, то внутреннее делегирование будет выполнено созданному узлу. В противном случае для вызова rememberUpdatedInstance будет использоваться Modifier.composed.