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() は使用されなくなり、代わりに新しいリップル API が使用されます。その結果、LocalRippleTheme のクエリは行われません。そのため、アプリケーションで LocalRippleTheme を設定しても、マテリアル コンポーネントはこれらの値を使用しません

以降のセクションでは、新しい 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 から移行する

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 {
            // ...
        }
    }

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

CompositionLocalProvider(LocalRippleConfiguration provides null) {
    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-ripplecreateRippleModifierNode() 関数を公開するようになりました。この関数を使用すると、デザイン システム ライブラリで高階 wrapper 実装を作成できます。この実装では、テーマ値をクエリしてから、この関数で作成されたノードにリップル実装を委任します。

これにより、デザイン システムは必要なものを直接クエリし、material-ripple レイヤで提供されるものに準拠することなく、必要なユーザー構成可能なテーマ設定レイヤを最上位に公開できます。また、この変更により、テーマから暗黙的に派生するのではなく、リップル API 自体がコントラクトを定義するため、リップルが準拠するテーマ/仕様がより明示的になります。

ガイダンスについては、マテリアル ライブラリのリップル API の実装を参照し、必要に応じて、独自のデザイン システムに合わせてマテリアル コンポジション ローカルの呼び出しを置き換えてください。

Indication から IndicationNodeFactory に移行する

Indication を渡す

Modifier.clickableModifier.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 かどうかを確認するように実装を更新する必要があります。これにより、より軽量な実装を使用できます。たとえば、Modifier.indicationIndicationNodeFactory の場合、内部的に作成されたノードに委任します。そうでない場合は、Modifier.composed を使用して rememberUpdatedInstance を呼び出します。