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

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

В следующем разделе описывается, как временно вернуться к старому поведению без миграции; однако мы рекомендуем перейти на новые API. Инструкции по миграции см. в разделе «Миграция с rememberRipple на ripple и последующих разделах.

Обновите версию библиотеки материалов без миграции

Чтобы разблокировать обновление версий библиотеки, вы можете использовать временный API LocalUseFallbackRippleImplementation CompositionLocal , чтобы настроить компоненты Material для возврата к старому поведению:

CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
    MaterialTheme {
        App()
    }
}

Обязательно укажите это вне MaterialTheme , чтобы старые пульсации можно было предоставить через LocalIndication .

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

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

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

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

Миграция с RippleTheme

Временно отказаться от изменения поведения

Библиотеки материалов имеют временный CompositionLocal , LocalUseFallbackRippleImplementation , который вы можете использовать для настройки всех компонентов Material для возврата к использованию rememberRipple . Таким образом, rememberRipple продолжает запрашивать LocalRippleTheme .

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

CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
    MaterialTheme {
        App()
    }
}

Если вы используете собственную тему приложения, созданную на основе Material, вы можете безопасно предоставить локальную композицию как часть темы вашего приложения:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
        MaterialTheme(content = content)
    }
}

Дополнительные сведения см. в разделе «Обновление версии библиотеки материалов без миграции» .

Использование 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 для определения поведения пульсации на уровне всей темы. По сути, это была точка интеграции между местными компонентами системы индивидуального проектирования и Ripple. Вместо предоставления общего примитива темы, material-ripple теперь предоставляет функцию createRippleModifierNode() . Эта функция позволяет библиотекам системы проектирования создавать реализацию wrapper более высокого порядка, которая запрашивает значения своей темы, а затем делегирует реализацию пульсации узлу, созданному этой функцией.

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

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

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

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