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

Вместо этого в библиотеках материалов предоставлены новые API ripple() .

Примечание: в данном контексте «библиотеки материалов» относятся к androidx.compose.material:material , androidx.compose.material3:material3 , androidx.wear.compose:compose-material и androidx.wear.compose:compose-material3.

RippleTheme

Или:

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

На этой странице описывается влияние изменения поведения и инструкции по переходу на новые 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() больше не является компонуемой функцией и не требует запоминания. Её также можно использовать повторно в нескольких компонентах, подобно модификаторам, поэтому рассмотрите возможность извлечения создания ripple в значение верхнего уровня для экономии ресурсов.

Внедрение системы индивидуального дизайна

Если вы реализуете собственную систему дизайна и ранее использовали rememberRipple() вместе с пользовательской темой RippleTheme для настройки Ripple, вам следует предоставить собственный API Ripple, который делегирует функции API узла Ripple, представленные в material-ripple . Тогда ваши компоненты смогут использовать вашу собственную 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 и предназначены только для настройки отдельных компонентов.

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

Миграция из 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 .