Переход на 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()

Вместо них в библиотеках Material Design появились новые API ripple().

Примечание. В этом контексте под библиотеками Material Design подразумеваются 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 Ripple. В результате они не запрашивают LocalRippleTheme. Поэтому, если вы зададите LocalRippleTheme в своем приложении, компоненты Material не будут использовать эти значения.

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

Переход с rememberRipple на ripple

Использование библиотеки Material

Если вы используете библиотеку Material, замените rememberRipple() вызовом ripple() из соответствующей библиотеки. Этот API создает пульсацию, используя значения, полученные из API тематического оформления Material Design. Затем передайте возвращенный объект в 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 пульсации определяет этот контракт, а не неявно выводится из темы.

Для получения рекомендаций ознакомьтесь с реализацией Ripple 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 очень похож на 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.