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 라이브러리에 새로운 ripple() API가 제공됩니다.

참고: 이 컨텍스트에서 'Material 라이브러리'는 androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material, androidx.wear.compose:compose-material3.를 의미합니다.

RippleTheme

다음 중 하나를 수행합니다.

  • Material 라이브러리 RippleConfiguration API를 사용합니다.
  • 자체 디자인 시스템 물결 효과 구현 빌드

이 페이지에서는 동작 변경사항의 영향과 새 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 라이브러리 사용

Material 라이브러리를 사용하는 경우 rememberRipple()를 해당 라이브러리의 ripple() 호출로 직접 대체합니다. 이 API는 Material Theming API에서 파생된 값을 사용하여 물결 효과를 만듭니다. 그런 다음 반환된 객체를 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와 함께 사용하여 물결 효과를 구성한 경우 material-ripple에 노출된 물결 효과 노드 API에 위임하는 자체 물결 효과 API를 대신 제공해야 합니다. 그러면 구성요소에서 테마 값을 직접 사용하는 자체 물결 효과를 사용할 수 있습니다. 자세한 내용은 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 자체이기 때문입니다.

자세한 내용은 Material 라이브러리의 ripple API 구현을 참고하고 자체 디자인 시스템에 필요한 대로 Material 컴포지션 로컬 호출을 대체하세요.

Indication에서 IndicationNodeFactory(으)로 이전

Indication 무렵에 지나감

Modifier.clickable 또는 Modifier.indication에 전달할 물결 효과를 만드는 등 전달할 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로 이전합니다. DrawModifierNode의 API 노출 영역은 IndicationInstance과 매우 유사합니다. IndicationInstance#drawContent()와 기능적으로 동일한 ContentDrawScope#draw() 함수를 노출합니다. 이 함수를 변경한 다음 Indication 대신 노드 내에서 직접 collectLatest 로직을 구현해야 합니다.

    예를 들어 다음 스니펫은 지원 중단된 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를 표시해야 합니다. 하지만 rememberUpdatedInstance를 사용하여 IndicationInstance를 수동으로 만드는 드문 경우에는 Indication가 IndicationNodeFactory인지 확인하도록 구현을 업데이트하여 더 가벼운 구현을 사용할 수 있습니다. 예를 들어 Modifier.indication는 IndicationNodeFactory인 경우 생성된 노드에 내부적으로 위임합니다. 그렇지 않으면 Modifier.composed를 사용하여 rememberUpdatedInstance를 호출합니다.