androidx.compose.material3.ripple

Interfaces

RippleNodeConfiguration.DragConfiguration

Represents the configuration for the visual representation of drag

Cmn
RippleNodeConfiguration.FocusConfiguration

Represents the configuration for the visual representation of focus

Cmn
RippleNodeConfiguration.HoverConfiguration

Represents the configuration for the visual representation of hover

Cmn
RippleNodeConfiguration.PressConfiguration

Represents the configuration for the visual representation of a press

Cmn

Classes

RippleNodeConfiguration

The configuration for the ripple node created by createRippleModifierNode.

Cmn
RippleNodeConfiguration.DragConfiguration.Opacity

An opacity-based drag visual.

Cmn
RippleNodeConfiguration.FocusConfiguration.InsetRing

An inset ring focus visual - the created ripple will show an inset focus ring.

Cmn
RippleNodeConfiguration.FocusConfiguration.Opacity

An opacity-based focus visual.

Cmn
RippleNodeConfiguration.HoverConfiguration.Opacity

An opacity-based hover visual.

Cmn
RippleNodeConfiguration.PressConfiguration.Opacity

An opacity-based press visual - the created ripple will show a layer with the given alpha on a press.

Cmn

Objects

RippleNodeConfiguration.DragConfiguration.None

No drag visual - the created ripple will not show anything for drag.

Cmn
RippleNodeConfiguration.FocusConfiguration.None

No focus visual - the created ripple will not show anything for focus.

Cmn
RippleNodeConfiguration.HoverConfiguration.None

No hover visual - the created ripple will not show anything for hover.

Cmn
RippleNodeConfiguration.PressConfiguration.None

No press visual - the created ripple will not show anything for press.

Cmn

Top-level functions summary

DelegatableNode
createRippleModifierNode(
    interactionSource: InteractionSource,
    rippleNodeConfiguration: () -> RippleNodeConfiguration
)

Creates a Ripple node using the values provided.

Cmn

Top-level functions

createRippleModifierNode

fun createRippleModifierNode(
    interactionSource: InteractionSource,
    rippleNodeConfiguration: () -> RippleNodeConfiguration
): DelegatableNode

Creates a Ripple node using the values provided.

A Ripple is a Material 3 node that expresses different Interactions by drawing ripple animations, and state layers, and other graphical effects.

A Ripple responds to PressInteraction.Press by starting a new RippleAnimation, and responds to other Interactions by showing a fixed state layer with varying alpha values depending on the Interaction, or an inset ring for FocusInteraction, depending on the supplied RippleNodeConfiguration.

This Ripple node is a low level building block for building IndicationNodeFactory implementations that use a Ripple - higher level design system libraries such as material3 provide ripple implementations using this node internally. In most cases you should use those factories directly: this node exists for design system libraries to delegate their Ripple implementation to, after querying any required theme values for customizing the Ripple.

NOTE: when using this factory with DelegatingNode.delegate, ensure that the node is created once or DelegatingNode.undelegate is called in Modifier.Node.onDetach. Repeatedly delegating to a new node returned by this method in Modifier.Node.onAttach without removing the old one will result in multiple ripple nodes being attached to the node.

import androidx.compose.foundation.IndicationNodeFactory
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ripple.RippleNodeConfiguration
import androidx.compose.material3.ripple.createRippleModifierNode
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorProducer
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.observeReads
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

/** Example CompositionLocals for a custom design system theme. */
val LocalCustomRippleColor: ProvidableCompositionLocal<Color> = compositionLocalOf { Color.Red }
val LocalCustomRippleEnabled: ProvidableCompositionLocal<Boolean> = compositionLocalOf { true }

/**
 * A custom [Modifier.Node] that queries design system theme values and delegates to
 * [createRippleModifierNode].
 */
class CustomRippleNode(
    private val interactionSource: InteractionSource,
    private val bounded: Boolean,
    private val radius: Dp,
    private val color: Color,
) : DelegatingNode(), CompositionLocalConsumerModifierNode, ObserverModifierNode {
    private var rippleNode: DelegatableNode? = null

    private val calculateColor = ColorProducer {
        if (color.isSpecified) {
            color
        } else {
            currentValueOf(LocalCustomRippleColor)
        }
    }

    private val rippleNodeConfiguration =
        RippleNodeConfiguration(
            isBounded = bounded,
            radius = radius,
            color = calculateColor,
            pressConfiguration =
                RippleNodeConfiguration.PressConfiguration.Opacity(alpha = 0.24f),
            focusConfiguration =
                RippleNodeConfiguration.FocusConfiguration.Opacity(alpha = 0.24f),
            hoverConfiguration =
                RippleNodeConfiguration.HoverConfiguration.Opacity(alpha = 0.08f),
            dragConfiguration =
                RippleNodeConfiguration.DragConfiguration.Opacity(alpha = 0.16f),
        )

    override fun onAttach() {
        updateConfiguration()
    }

    override fun onObservedReadsChanged() {
        updateConfiguration()
    }

    private fun updateConfiguration() {
        observeReads {
            val isEnabled = currentValueOf(LocalCustomRippleEnabled)
            if (isEnabled) {
                if (rippleNode == null) {
                    attachNewRipple()
                }
            } else {
                removeRipple()
            }
        }
    }

    private fun attachNewRipple() {
        rippleNode =
            delegate(
                createRippleModifierNode(
                    interactionSource = interactionSource,
                    rippleNodeConfiguration = { rippleNodeConfiguration },
                )
            )
    }

    private fun removeRipple() {
        rippleNode?.let { undelegate(it) }
        rippleNode = null
    }
}

/** A custom [IndicationNodeFactory] that creates a [CustomRippleNode]. */
class CustomRipple(
    private val bounded: Boolean = true,
    private val radius: Dp = Dp.Unspecified,
    private val color: Color = Color.Unspecified,
) : IndicationNodeFactory {
    override fun create(interactionSource: InteractionSource): DelegatableNode {
        return CustomRippleNode(interactionSource, bounded, radius, color)
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is CustomRipple) return false
        if (bounded != other.bounded) return false
        if (radius != other.radius) return false
        if (color != other.color) return false
        return true
    }

    override fun hashCode(): Int {
        var result = bounded.hashCode()
        result = 31 * result + radius.hashCode()
        result = 31 * result + color.hashCode()
        return result
    }
}

val interactionSource = remember { MutableInteractionSource() }

Box(
    modifier =
        Modifier.size(100.dp)
            .clickable(
                interactionSource = interactionSource,
                indication = CustomRipple(color = Color.Blue),
                onClick = {},
            )
)
Parameters
interactionSource: InteractionSource

the InteractionSource used to determine the state of the ripple.

rippleNodeConfiguration: () -> RippleNodeConfiguration

the RippleNodeConfiguration that will be applied to the ripple depending on the state of the ripple. This lambda may be invoked repeatedly, so consider caching values of configuration when they haven't changed.

Returns
DelegatableNode

a DelegatableNode that handles drawing the ripple and state layers.