/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.HoverInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.tokens.ElevatedCardTokens
import androidx.compose.material3.tokens.FilledCardTokens
import androidx.compose.material3.tokens.OutlinedCardTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import kotlinx.coroutines.flow.collect
/**
* <a href="https://m3.material.io/components/cards/overview" class="external" target="_blank">Material Design filled card</a>.
*
* Cards contain content and actions about a single subject. Filled cards provide subtle separation
* from the background. This has less emphasis than elevated or outlined cards.
*
* This Card does not handle input events - see the other Card overloads if you want a clickable or
* selectable Card.
*
* Card sample:
* @sample androidx.compose.material3.samples.CardSample
*
* @param modifier Modifier to be applied to the layout of the card.
* @param shape Defines the card's shape.
* @param containerColor The container color of the card.
* @param contentColor The preferred content color provided by this card to its children.
* Defaults to either the matching content color for [containerColor], or if [containerColor]
* is not a color from the theme, this will keep the same value set above this card.
* @param border [BorderStroke] to draw on top of the card.
* @param elevation [CardElevation] used to resolve the elevation for this card. The resolved value
* control the size of the shadow below the card, as well as its tonal elevation. When
* [containerColor] is [ColorScheme.surface], a higher tonal elevation value will result in a darker
* card color in light theme and lighter color in dark theme. See also [Surface].
*/
@ExperimentalMaterial3Api
@Composable
fun Card(
modifier: Modifier = Modifier,
shape: Shape = FilledCardTokens.ContainerShape.toShape(),
containerColor: Color = FilledCardTokens.ContainerColor.toColor(),
contentColor: Color = contentColorFor(containerColor),
border: BorderStroke? = null,
elevation: CardElevation = CardDefaults.cardElevation(),
content: @Composable ColumnScope.() -> Unit
) {
Surface(
modifier = modifier,
shape = shape,
color = containerColor,
contentColor = contentColor,
tonalElevation = elevation.tonalElevation(interactionSource = null).value,
shadowElevation = elevation.shadowElevation(interactionSource = null).value,
border = border,
) {
Column(content = content)
}
}
/**
* <a href="https://m3.material.io/components/cards/overview" class="external" target="_blank">Material Design filled card</a>.
*
* Cards contain content and actions about a single subject. Filled cards provide subtle separation
* from the background. This has less emphasis than elevated or outlined cards.
*
* This Card handles click events, calling its [onClick] lambda.
*
* Clickable card sample:
* @sample androidx.compose.material3.samples.ClickableCardSample
*
* @param onClick callback to be called when the card is clicked
* @param modifier Modifier to be applied to the layout of the card.
* @param interactionSource the [MutableInteractionSource] representing the stream of
* [Interaction]s for this Card. You can create and pass in your own remembered
* [MutableInteractionSource] to observe [Interaction]s that will customize the appearance
* / behavior of this card in different states.
* @param shape Defines the card's shape.
* @param containerColor The container color of the card.
* @param contentColor The preferred content color provided by this card to its children.
* Defaults to either the matching content color for [containerColor], or if [containerColor]
* is not a color from the theme, this will keep the same value set above this card.
* @param border [BorderStroke] to draw on top of the card.
* @param elevation [CardElevation] used to resolve the elevation for this card when the
* [interactionSource] emits its states. The resolved values control the size of the shadow below
* the card, as well as its tonal elevation. When [containerColor] is [ColorScheme.surface], a
* higher tonal elevation value will result in a darker card color in light theme and lighter color
* in dark theme. See also [Surface].
*/
@ExperimentalMaterial3Api
@Composable
fun Card(
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = FilledCardTokens.ContainerShape.toShape(),
containerColor: Color = FilledCardTokens.ContainerColor.toColor(),
contentColor: Color = contentColorFor(containerColor),
border: BorderStroke? = null,
elevation: CardElevation = CardDefaults.cardElevation(),
content: @Composable ColumnScope.() -> Unit
) {
Surface(
onClick = onClick,
modifier = modifier,
shape = shape,
color = containerColor,
contentColor = contentColor,
tonalElevation = elevation.tonalElevation(interactionSource).value,
shadowElevation = elevation.shadowElevation(interactionSource).value,
border = border,
interactionSource = interactionSource,
) {
Column(content = content)
}
}
/**
* <a href="https://m3.material.io/components/cards/overview" class="external" target="_blank">Material Design elevated card</a>.
*
* Elevated cards contain content and actions about a single subject. They have a drop shadow,
* providing more separation from the background than filled cards, but less than outlined cards.
*
* This ElevatedCard does not handle input events - see the other ElevatedCard overloads if you
* want a clickable or selectable ElevatedCard.
*
* Elevated card sample:
* @sample androidx.compose.material3.samples.ElevatedCardSample
*
* @param modifier Modifier to be applied to the layout of the card.
* @param shape Defines the card's shape.
* @param containerColor The container color of the card.
* @param contentColor The preferred content color provided by this card to its children.
* Defaults to either the matching content color for [containerColor], or if [containerColor]
* is not a color from the theme, this will keep the same value set above this card.
* @param elevation [CardElevation] used to resolve the elevation for this card. The resolved value
* control the size of the shadow below the card, as well as its tonal elevation. When
* [containerColor] is [ColorScheme.surface], a higher tonal elevation value will result in a darker
* card color in light theme and lighter color in dark theme. See also [Surface].
*/
@ExperimentalMaterial3Api
@Composable
fun ElevatedCard(
modifier: Modifier = Modifier,
shape: Shape = ElevatedCardTokens.ContainerShape.toShape(),
containerColor: Color = ElevatedCardTokens.ContainerColor.toColor(),
contentColor: Color = contentColorFor(containerColor),
elevation: CardElevation = CardDefaults.elevatedCardElevation(),
content: @Composable ColumnScope.() -> Unit
) = Card(
modifier = modifier,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
border = null,
elevation = elevation,
content = content
)
/**
* <a href="https://m3.material.io/components/cards/overview" class="external" target="_blank">Material Design elevated card</a>.
*
* Elevated cards contain content and actions about a single subject. They have a drop shadow,
* providing more separation from the background than filled cards, but less than outlined cards.
*
* This ElevatedCard handles click events, calling its [onClick] lambda.
*
* Clickable elevated card sample:
* @sample androidx.compose.material3.samples.ClickableElevatedCardSample
*
* @param onClick callback to be called when the card is clicked
* @param modifier Modifier to be applied to the layout of the card.
* @param interactionSource the [MutableInteractionSource] representing the stream of
* [Interaction]s for this card. You can create and pass in your own remembered
* [MutableInteractionSource] to observe [Interaction]s that will customize the appearance
* / behavior of this card in different states.
* @param shape Defines the card's shape.
* @param containerColor The container color of the card.
* @param contentColor The preferred content color provided by this card to its children.
* Defaults to either the matching content color for [containerColor], or if [containerColor]
* is not a color from the theme, this will keep the same value set above this card.
* @param elevation [CardElevation] used to resolve the elevation for this card when the
* [interactionSource] emits its states. The resolved values control the size of the shadow below
* the card, as well as its tonal elevation. When [containerColor] is [ColorScheme.surface], a
* higher tonal elevation value will result in a darker card color in light theme and lighter color
* in dark theme. See also [Surface].
*/
@ExperimentalMaterial3Api
@Composable
fun ElevatedCard(
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = ElevatedCardTokens.ContainerShape.toShape(),
containerColor: Color = ElevatedCardTokens.ContainerColor.toColor(),
contentColor: Color = contentColorFor(containerColor),
elevation: CardElevation = CardDefaults.elevatedCardElevation(),
content: @Composable ColumnScope.() -> Unit
) = Card(
onClick = onClick,
modifier = modifier,
interactionSource = interactionSource,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
border = null,
elevation = elevation,
content = content
)
/**
* <a href="https://m3.material.io/components/cards/overview" class="external" target="_blank">Material Design outlined card</a>.
*
* Outlined cards contain content and actions about a single subject. They have a visual boundary
* around the container. This can provide greater emphasis than the other types.
*
* This OutlinedCard does not handle input events - see the other OutlinedCard overloads if you want
* a clickable or selectable OutlinedCard.
*
* Outlined card sample:
* @sample androidx.compose.material3.samples.OutlinedCardSample
*
* @param modifier Modifier to be applied to the layout of the card.
* @param shape Defines the card's shape.
* @param containerColor The container color of the card.
* @param contentColor The preferred content color provided by this card to its children.
* Defaults to either the matching content color for [containerColor], or if [containerColor]
* is not a color from the theme, this will keep the same value set above this card.
* @param border [BorderStroke] to draw on top of the card.
* @param elevation [CardElevation] used to resolve the elevation for this card. The resolved value
* control the size of the shadow below the card, as well as its tonal elevation. When
* [containerColor] is [ColorScheme.surface], a higher tonal elevation value will result in a darker
* card color in light theme and lighter color in dark theme. See also [Surface].
*/
@ExperimentalMaterial3Api
@Composable
fun OutlinedCard(
modifier: Modifier = Modifier,
shape: Shape = OutlinedCardTokens.ContainerShape.toShape(),
containerColor: Color = OutlinedCardTokens.ContainerColor.toColor(),
contentColor: Color = contentColorFor(containerColor),
border: BorderStroke = BorderStroke(
OutlinedCardTokens.OutlineWidth,
OutlinedCardTokens.OutlineColor.toColor()
),
elevation: CardElevation = CardDefaults.outlinedCardElevation(),
content: @Composable ColumnScope.() -> Unit
) = Card(
modifier = modifier,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
border = border,
elevation = elevation,
content = content
)
/**
* <a href="https://m3.material.io/components/cards/overview" class="external" target="_blank">Material Design outlined card</a>.
*
* Outlined cards contain content and actions about a single subject. They have a visual boundary
* around the container. This can provide greater emphasis than the other types.
*
* This OutlinedCard handles click events, calling its [onClick] lambda.
*
* Clickable outlined card sample:
* @sample androidx.compose.material3.samples.ClickableOutlinedCardSample
*
* @param onClick callback to be called when the card is clicked
* @param modifier Modifier to be applied to the layout of the card.
* @param interactionSource the [MutableInteractionSource] representing the stream of
* [Interaction]s for this card. You can create and pass in your own remembered
* [MutableInteractionSource] to observe [Interaction]s that will customize the appearance
* / behavior of this card in different states.
* @param shape Defines the card's shape.
* @param containerColor The container color of the card.
* @param contentColor The preferred content color provided by this card to its children.
* Defaults to either the matching content color for [containerColor], or if [containerColor]
* is not a color from the theme, this will keep the same value set above this card.
* @param border [BorderStroke] to draw on top of the card.
* @param elevation [CardElevation] used to resolve the elevation for this card when the
* [interactionSource] emits its states. The resolved values control the size of the shadow below
* the card, as well as its tonal elevation. When [containerColor] is [ColorScheme.surface], a
* higher tonal elevation value will result in a darker card color in light theme and lighter color
* in dark theme. See also [Surface].
*/
@ExperimentalMaterial3Api
@Composable
fun OutlinedCard(
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = OutlinedCardTokens.ContainerShape.toShape(),
containerColor: Color = OutlinedCardTokens.ContainerColor.toColor(),
contentColor: Color = contentColorFor(containerColor),
border: BorderStroke = BorderStroke(
OutlinedCardTokens.OutlineWidth,
OutlinedCardTokens.OutlineColor.toColor()
),
elevation: CardElevation = CardDefaults.outlinedCardElevation(),
content: @Composable ColumnScope.() -> Unit
) = Card(
onClick = onClick,
modifier = modifier,
interactionSource = interactionSource,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
border = border,
elevation = elevation,
content = content
)
/**
* Represents the elevation for a card in different states.
*
* - See [CardDefaults.cardElevation] for the default elevation used in a [Card].
* - See [CardDefaults.elevatedCardElevation] for the default elevation used in an [ElevatedCard].
* - See [CardDefaults.outlinedCardElevation] for the default elevation used in an [OutlinedCard].
*/
@Stable
interface CardElevation {
/**
* Represents the tonal elevation used in a card, depending on its [interactionSource].
*
* Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
*
* For all Material cards with elevation, this returns the same value as [shadowElevation].
*
* - See [shadowElevation] for an elevation that draws a shadow around the card's bounds.
*
* @param interactionSource the [InteractionSource] for this card
*/
@Composable
fun tonalElevation(interactionSource: InteractionSource?): State<Dp>
/**
* Represents the shadow elevation used in a card, depending on the [interactionSource].
*
* Shadow elevation is used to apply a drop shadow around the card to give it higher emphasis.
*
* For all Material cards with elevation, this returns the same value as [tonalElevation].
*
* - See [tonalElevation] for an elevation that applies a color shift to the surface.
*
* @param interactionSource the [InteractionSource] for this card
*/
@Composable
fun shadowElevation(interactionSource: InteractionSource?): State<Dp>
}
/**
* Contains the default values used by all card types.
*/
object CardDefaults {
/**
* Creates a [CardElevation] that will animate between the provided values according to the
* Material specification for a [Card].
*
* @param defaultElevation the elevation used when the [Card] is has no other [Interaction]s.
* @param pressedElevation the elevation used when the [Card] is pressed.
* @param focusedElevation the elevation used when the [Card] is focused.
* @param hoveredElevation the elevation used when the [Card] is hovered.
* @param draggedElevation the elevation used when the [Card] is dragged.
*/
@Composable
fun cardElevation(
defaultElevation: Dp = FilledCardTokens.ContainerElevation,
pressedElevation: Dp = FilledCardTokens.PressedContainerElevation,
focusedElevation: Dp = FilledCardTokens.FocusContainerElevation,
hoveredElevation: Dp = FilledCardTokens.HoverContainerElevation,
draggedElevation: Dp = FilledCardTokens.DraggedContainerElevation
): CardElevation {
return remember(
defaultElevation,
pressedElevation,
focusedElevation,
hoveredElevation,
draggedElevation
) {
DefaultCardElevation(
defaultElevation = defaultElevation,
pressedElevation = pressedElevation,
focusedElevation = focusedElevation,
hoveredElevation = hoveredElevation,
draggedElevation = draggedElevation,
)
}
}
/**
* Creates a [CardElevation] that will animate between the provided values according to the
* Material specification for an [ElevatedCard].
*
* @param defaultElevation the elevation used when the [ElevatedCard] is has no other
* [Interaction]s.
* @param pressedElevation the elevation used when the [ElevatedCard] is pressed.
* @param focusedElevation the elevation used when the [ElevatedCard] is focused.
* @param hoveredElevation the elevation used when the [ElevatedCard] is hovered.
* @param draggedElevation the elevation used when the [ElevatedCard] is dragged.
*/
@Composable
fun elevatedCardElevation(
defaultElevation: Dp = ElevatedCardTokens.ContainerElevation,
pressedElevation: Dp = ElevatedCardTokens.PressedContainerElevation,
focusedElevation: Dp = ElevatedCardTokens.FocusContainerElevation,
hoveredElevation: Dp = ElevatedCardTokens.HoverContainerElevation,
draggedElevation: Dp = ElevatedCardTokens.DraggedContainerElevation
): CardElevation {
return remember(
defaultElevation,
pressedElevation,
focusedElevation,
hoveredElevation,
draggedElevation
) {
DefaultCardElevation(
defaultElevation = defaultElevation,
pressedElevation = pressedElevation,
focusedElevation = focusedElevation,
hoveredElevation = hoveredElevation,
draggedElevation = draggedElevation,
)
}
}
/**
* Creates a [CardElevation] that will animate between the provided values according to the
* Material specification for an [OutlinedCard].
*
* @param defaultElevation the elevation used when the [OutlinedCard] is has no other
* [Interaction]s.
* @param pressedElevation the elevation used when the [OutlinedCard] is pressed.
* @param focusedElevation the elevation used when the [OutlinedCard] is focused.
* @param hoveredElevation the elevation used when the [OutlinedCard] is hovered.
* @param draggedElevation the elevation used when the [OutlinedCard] is dragged.
*/
@Composable
fun outlinedCardElevation(
defaultElevation: Dp = OutlinedCardTokens.ContainerElevation,
pressedElevation: Dp = defaultElevation,
focusedElevation: Dp = defaultElevation,
hoveredElevation: Dp = defaultElevation,
draggedElevation: Dp = OutlinedCardTokens.DraggedContainerElevation
): CardElevation {
return remember(
defaultElevation,
pressedElevation,
focusedElevation,
hoveredElevation,
draggedElevation
) {
DefaultCardElevation(
defaultElevation = defaultElevation,
pressedElevation = pressedElevation,
focusedElevation = focusedElevation,
hoveredElevation = hoveredElevation,
draggedElevation = draggedElevation,
)
}
}
}
/**
* Default [CardElevation] implementation.
*
* This default implementation supports animating the elevation for pressed, focused, hovered, and
* dragged interactions.
*/
@Immutable
private class DefaultCardElevation(
private val defaultElevation: Dp,
private val pressedElevation: Dp,
private val focusedElevation: Dp,
private val hoveredElevation: Dp,
private val draggedElevation: Dp,
) : CardElevation {
@Composable
override fun tonalElevation(interactionSource: InteractionSource?): State<Dp> {
if (interactionSource == null) {
return remember { mutableStateOf(defaultElevation) }
}
return animateElevation(interactionSource = interactionSource)
}
@Composable
override fun shadowElevation(interactionSource: InteractionSource?): State<Dp> {
if (interactionSource == null) {
return remember { mutableStateOf(defaultElevation) }
}
return animateElevation(interactionSource = interactionSource)
}
@Composable
private fun animateElevation(interactionSource: InteractionSource): State<Dp> {
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is HoverInteraction.Enter -> {
interactions.add(interaction)
}
is HoverInteraction.Exit -> {
interactions.remove(interaction.enter)
}
is FocusInteraction.Focus -> {
interactions.add(interaction)
}
is FocusInteraction.Unfocus -> {
interactions.remove(interaction.focus)
}
is PressInteraction.Press -> {
interactions.add(interaction)
}
is PressInteraction.Release -> {
interactions.remove(interaction.press)
}
is PressInteraction.Cancel -> {
interactions.remove(interaction.press)
}
is DragInteraction.Start -> {
interactions.add(interaction)
}
is DragInteraction.Stop -> {
interactions.remove(interaction.start)
}
is DragInteraction.Cancel -> {
interactions.remove(interaction.start)
}
}
}
}
val interaction = interactions.lastOrNull()
val target = when (interaction) {
is PressInteraction.Press -> pressedElevation
is HoverInteraction.Enter -> hoveredElevation
is FocusInteraction.Focus -> focusedElevation
is DragInteraction.Start -> draggedElevation
else -> defaultElevation
}
val animatable = remember { Animatable(target, Dp.VectorConverter) }
LaunchedEffect(target) {
val lastInteraction = when (animatable.targetValue) {
pressedElevation -> PressInteraction.Press(Offset.Zero)
hoveredElevation -> HoverInteraction.Enter()
focusedElevation -> FocusInteraction.Focus()
draggedElevation -> DragInteraction.Start()
else -> null
}
animatable.animateElevation(
from = lastInteraction,
to = interaction,
target = target
)
}
return animatable.asState()
}
}