Переход на API индикации и Ripple

Для повышения производительности композиции интерактивных компонентов, использующих 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 предоставляются новые API-интерфейсы ripple() .

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

RippleTheme

Или:

  • Используйте API RippleConfiguration из библиотеки Material, или
  • Создайте собственную систему проектирования и реализуйте эффект пульсации.

На этой странице описывается влияние изменений в поведении пользователей и приводятся инструкции по переходу на новые 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, замените метод 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 предоставляют API-интерфейсы 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 . Это изменение также делает более явным, какой теме/спецификации соответствует Ripple, поскольку именно API Ripple определяет этот контракт, а не выводится неявно из темы.

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

Использование Indication для создания IndicationInstance

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