Indication API と 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()

代わりにマテリアル ライブラリで提供される新しい ripple() API。

注: このコンテキストにおける「マテリアル ライブラリ」とは、androidx.compose.material:materialandroidx.compose.material3:material3androidx.wear.compose:compose-materialandroidx.wear.compose:compose-material3. を指します。

RippleTheme

次のいずれかの手順を行います。

  • マテリアル ライブラリの 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+

これらのバージョンのマテリアル ライブラリでは、rememberRipple() を使用せず、代わりに新しい ripple API を使用します。そのため、LocalRippleTheme はクエリされません。したがって、アプリで LocalRippleTheme を設定した場合、マテリアル コンポーネントはこれらの値を使用しません

次のセクションでは、移行せずに一時的に以前の動作にフォールバックする方法について説明しますが、新しい API に移行することをおすすめします。移行手順については、rememberRipple から ripple に移行すると後続のセクションをご覧ください。

移行せずにマテリアル ライブラリのバージョンをアップグレードする

ライブラリ バージョンのアップグレードのブロックを解除するには、一時的な LocalUseFallbackRippleImplementation CompositionLocal API を使用して、以前の動作にフォールバックするようにマテリアル コンポーネントを構成します。

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

LocalIndication を通じて古いリップルを提供できるように、必ず MaterialTheme の外部で提供してください。

以降のセクションでは、新しい API に移行する方法について説明します。

rememberRipple から ripple への移行

マテリアル ライブラリの使用

マテリアル ライブラリを使用している場合は、rememberRipple() を、対応するライブラリからの ripple() の呼び出しに直接置き換えます。この API は、マテリアル テーマ 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 からの移行

動作変更を一時的にオプトアウトする

マテリアル ライブラリには一時的な CompositionLocalLocalUseFallbackRippleImplementation があります。これを使用すると、すべてのマテリアル コンポーネントを構成して、rememberRipple を使用するようフォールバックできます。これにより、rememberRipple は引き続き LocalRippleTheme をクエリします。

次のコード スニペットは、LocalUseFallbackRippleImplementation CompositionLocal API の使用方法を示しています。

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

マテリアル上に構築されたアプリのカスタムテーマを使用している場合は、アプリのテーマの一部としてコンポジション ローカルを安全に提供できます。

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

詳しくは、移行せずにマテリアル ライブラリのバージョンをアップグレードするをご覧ください。

RippleTheme を使用して特定のコンポーネントのリップルを無効にする

material ライブラリと material3 ライブラリは RippleConfigurationLocalRippleConfiguration を公開し、サブツリー内の波紋の外観を設定できます。RippleConfigurationLocalRippleConfiguration は試験運用版であり、コンポーネントごとのカスタマイズのみを目的としています。これらの 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 {
            // ...
        }
    }

上記のスニペットを次のように変更する必要があります。

@OptIn(ExperimentalMaterialApi::class)
private val DisabledRippleConfiguration =
    RippleConfiguration(isEnabled = false)

// ...
    CompositionLocalProvider(LocalRippleConfiguration provides DisabledRippleConfiguration) {
        Button {
            // ...
        }
    }

RippleTheme を使用して特定のコンポーネントのリップルの色/アルファを変更する

前のセクションで説明したように、RippleConfigurationLocalRippleConfiguration は試験運用版 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 レイヤで提供される内容に準拠することなく、必要なユーザー構成可能なテーマ設定レイヤを上位に公開できます。また、この変更により、リップルが準拠しているテーマや仕様がより明確になります。これは、テーマから暗黙的に派生するのではなく、リップル API 自体がそのコントラクトを定義するためです。

詳しくは、マテリアル ライブラリのリップル API 実装をご覧ください。また、独自のデザイン システムに合わせて、マテリアル コンポジション ローカルの呼び出しを置き換えます。

Indication から IndicationNodeFactory への移行

Indication を通過しています

Modifier.clickable または Modifier.indication に渡すリップルを作成するなど、渡す Indication を作成するだけの場合は、変更する必要はありません。IndicationNodeFactoryIndication から継承されるため、すべてが引き続きコンパイルされて機能します。

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()
        }
    }
}

これは次の 2 つのステップで移行できます。

  1. ScaleIndicationInstanceDrawModifierNode に移行します。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 を作成するまれなケースですが、実装を更新して、IndicationIndicationNodeFactory であるかどうかを確認して、より軽量な実装を使用できるようにする必要があります。たとえば、作成されたノードが IndicationNodeFactory の場合、Modifier.indication は作成したノードに内部的にデリゲートします。そうでない場合は、Modifier.composed を使用して rememberUpdatedInstance を呼び出します。