blob: 53add8d6425c151e1a548625bab56482b884b69a [file] [log] [blame]
/*
* Copyright 2023 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.tv.material3
import androidx.annotation.FloatRange
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
/**
* Cards contain content and actions that relate information about a subject.
*
* This Card handles click events, calling its [onClick] lambda.
*
* @param onClick called when this card is clicked
* @param modifier the [Modifier] to be applied to this card
* @param shape [CardShape] defines the shape of this card's container in different interaction
* states. See [CardDefaults.shape].
* @param colors [CardColors] defines the background & content colors used in this card for
* different interaction states. See [CardDefaults.colors].
* @param scale [CardScale] defines size of the card relative to its original size for different
* interaction states. See [CardDefaults.scale].
* @param border [CardBorder] defines a border around the card for different interaction states.
* See [CardDefaults.border].
* @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
* states. See [CardDefaults.glow].
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this card. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this card in different states.
* @param content defines the [Composable] content inside the Card.
*/
@ExperimentalTvMaterial3Api
@Composable
fun Card(
onClick: () -> Unit,
modifier: Modifier = Modifier,
shape: CardShape = CardDefaults.shape(),
colors: CardColors = CardDefaults.colors(),
scale: CardScale = CardDefaults.scale(),
border: CardBorder = CardDefaults.border(),
glow: CardGlow = CardDefaults.glow(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable ColumnScope.() -> Unit
) {
Surface(
onClick = onClick,
modifier = modifier,
shape = shape.toClickableSurfaceShape(),
color = colors.toClickableSurfaceContainerColor(),
contentColor = colors.toClickableSurfaceContentColor(),
scale = scale.toClickableSurfaceScale(),
border = border.toClickableSurfaceBorder(),
glow = glow.toClickableSurfaceGlow(),
interactionSource = interactionSource,
) {
Column(content = content)
}
}
/**
* [ClassicCard] is an opinionated TV Material card that offers a 4 slot layout to show
* information about a subject.
*
* This card has a vertical layout with the interactive surface [Surface], which provides the image
* slot at the top, followed by the title, subtitle, and description slots.
*
* This Card handles click events, calling its [onClick] lambda.
*
* @param onClick called when this card is clicked
* @param image defines the [Composable] image to be displayed on top of the Card.
* @param title defines the [Composable] title placed below the image in the Card.
* @param modifier the [Modifier] to be applied to this card.
* @param subtitle defines the [Composable] supporting text placed below the title of the Card.
* @param description defines the [Composable] description placed below the subtitle of the Card.
* @param shape [CardShape] defines the shape of this card's container in different interaction
* states. See [CardDefaults.shape].
* @param colors [CardColors] defines the background & content colors used in this card for
* different interaction states. See [CardDefaults.colors].
* @param scale [CardScale] defines size of the card relative to its original size for different
* interaction states. See [CardDefaults.scale].
* @param border [CardBorder] defines a border around the card for different interaction states.
* See [CardDefaults.border].
* @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
* states. See [CardDefaults.glow].
* @param contentPadding [PaddingValues] defines the inner padding applied to the card's content.
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this card. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this card in different states.
*/
@ExperimentalTvMaterial3Api
@Composable
fun ClassicCard(
onClick: () -> Unit,
image: @Composable BoxScope.() -> Unit,
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
subtitle: @Composable () -> Unit = {},
description: @Composable () -> Unit = {},
shape: CardShape = CardDefaults.shape(),
colors: CardColors = CardDefaults.colors(),
scale: CardScale = CardDefaults.scale(),
border: CardBorder = CardDefaults.border(),
glow: CardGlow = CardDefaults.glow(),
contentPadding: PaddingValues = PaddingValues(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
Card(
onClick = onClick,
modifier = modifier,
interactionSource = interactionSource,
shape = shape,
colors = colors,
scale = scale,
border = border,
glow = glow
) {
Column(
modifier = Modifier.padding(contentPadding)
) {
Box(
contentAlignment = CardDefaults.ContentImageAlignment,
content = image
)
Column {
CardContent(
title = title,
subtitle = subtitle,
description = description
)
}
}
}
}
/**
* [CompactCard] is an opinionated TV Material card that offers a 4 slot layout to show
* information about a subject.
*
* This card provides the interactive surface [Surface] with the image slot as the background
* (with an overlay scrim gradient). Other slots for the title, subtitle, and description are
* placed over it.
*
* This Card handles click events, calling its [onClick] lambda.
*
* @param onClick called when this card is clicked
* @param image defines the [Composable] image to be displayed on top of the Card.
* @param title defines the [Composable] title placed below the image in the Card.
* @param modifier the [Modifier] to be applied to this card.
* @param subtitle defines the [Composable] supporting text placed below the title of the Card.
* @param description defines the [Composable] description placed below the subtitle of the Card.
* @param shape [CardShape] defines the shape of this card's container in different interaction
* states. See [CardDefaults.shape].
* @param colors [CardColors] defines the background & content colors used in this card for
* different interaction states. See [CardDefaults.compactCardColors].
* @param scale [CardScale] defines size of the card relative to its original size for different
* interaction states. See [CardDefaults.scale].
* @param border [CardBorder] defines a border around the card for different interaction states.
* See [CardDefaults.border].
* @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
* states. See [CardDefaults.glow].
* @param scrimBrush [Brush] defines a brush/gradient to be used to draw the scrim over the image
* in the background. See [CardDefaults.ContainerGradient].
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this card. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this card in different states.
*/
@ExperimentalTvMaterial3Api
@Composable
fun CompactCard(
onClick: () -> Unit,
image: @Composable BoxScope.() -> Unit,
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
subtitle: @Composable () -> Unit = {},
description: @Composable () -> Unit = {},
shape: CardShape = CardDefaults.shape(),
colors: CardColors = CardDefaults.compactCardColors(),
scale: CardScale = CardDefaults.scale(),
border: CardBorder = CardDefaults.border(),
glow: CardGlow = CardDefaults.glow(),
scrimBrush: Brush = CardDefaults.ContainerGradient,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
Card(
onClick = onClick,
modifier = modifier,
interactionSource = interactionSource,
shape = shape,
colors = colors,
scale = scale,
border = border,
glow = glow
) {
Box(contentAlignment = Alignment.BottomStart) {
Box(
modifier = Modifier
.drawWithCache {
onDrawWithContent {
drawContent()
drawRect(brush = scrimBrush)
}
},
contentAlignment = CardDefaults.ContentImageAlignment,
content = image
)
Column {
CardContent(
title = title,
subtitle = subtitle,
description = description
)
}
}
}
}
/**
* [WideClassicCard] is an opinionated TV Material card that offers a 4 slot layout to show
* information about a subject.
*
* This card has a horizontal layout with the interactive surface [Surface], which provides the
* image slot at the start, followed by the title, subtitle, and description slots at the end.
*
* This Card handles click events, calling its [onClick] lambda.
*
* @param onClick called when this card is clicked
* @param image defines the [Composable] image to be displayed on top of the Card.
* @param title defines the [Composable] title placed below the image in the Card.
* @param modifier the [Modifier] to be applied to this card.
* @param subtitle defines the [Composable] supporting text placed below the title of the Card.
* @param description defines the [Composable] description placed below the subtitle of the Card.
* @param shape [CardShape] defines the shape of this card's container in different interaction
* states. See [CardDefaults.shape].
* @param colors [CardColors] defines the background & content colors used in this card for
* different interaction states. See [CardDefaults.colors].
* @param scale [CardScale] defines size of the card relative to its original size for different
* interaction states. See [CardDefaults.scale].
* @param border [CardBorder] defines a border around the card for different interaction states.
* See [CardDefaults.border].
* @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
* states. See [CardDefaults.glow].
* @param contentPadding [PaddingValues] defines the inner padding applied to the card's content.
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this card. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this card in different states.
*/
@ExperimentalTvMaterial3Api
@Composable
fun WideClassicCard(
onClick: () -> Unit,
image: @Composable BoxScope.() -> Unit,
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
subtitle: @Composable () -> Unit = {},
description: @Composable () -> Unit = {},
shape: CardShape = CardDefaults.shape(),
colors: CardColors = CardDefaults.colors(),
scale: CardScale = CardDefaults.scale(),
border: CardBorder = CardDefaults.border(),
glow: CardGlow = CardDefaults.glow(),
contentPadding: PaddingValues = PaddingValues(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
Card(
onClick = onClick,
modifier = modifier,
interactionSource = interactionSource,
shape = shape,
colors = colors,
scale = scale,
border = border,
glow = glow
) {
Row(
modifier = Modifier.padding(contentPadding)
) {
Box(
contentAlignment = CardDefaults.ContentImageAlignment,
content = image
)
Column {
CardContent(
title = title,
subtitle = subtitle,
description = description
)
}
}
}
}
@Composable
private fun CardContent(
title: @Composable () -> Unit,
subtitle: @Composable () -> Unit = {},
description: @Composable () -> Unit = {},
contentColor: Color
) {
CompositionLocalProvider(LocalContentColor provides contentColor) {
CardContent(title, subtitle, description)
}
}
@Composable
private fun CardContent(
title: @Composable () -> Unit,
subtitle: @Composable () -> Unit = {},
description: @Composable () -> Unit = {}
) {
ProvideTextStyle(MaterialTheme.typography.titleMedium) {
title.invoke()
}
ProvideTextStyle(MaterialTheme.typography.bodySmall) {
Box(Modifier.graphicsLayer { alpha = SubtitleAlpha }) {
subtitle.invoke()
}
}
ProvideTextStyle(MaterialTheme.typography.bodySmall) {
Box(Modifier.graphicsLayer { alpha = DescriptionAlpha }) {
description.invoke()
}
}
}
/**
* Contains the default values used by all card types.
*/
@ExperimentalTvMaterial3Api
object CardDefaults {
internal val ContentImageAlignment = Alignment.Center
/**
* The default [Shape] used by Cards.
*/
private val ContainerShape = RoundedCornerShape(8.dp)
/**
* Recommended aspect ratio [Float] to get square images, can be applied using the modifier
* [Modifier.aspectRatio].
*/
const val SquareImageAspectRatio = 1f
/**
* Recommended aspect ratio [Float] for vertical images, can be applied using the modifier
* [Modifier.aspectRatio].
*/
const val VerticalImageAspectRatio = 2f / 3
/**
* Recommended aspect ratio [Float] for horizontal images, can be applied using the modifier
* [Modifier.aspectRatio].
*/
const val HorizontalImageAspectRatio = 16f / 9
/**
* Gradient used in cards to give more emphasis to the textual content that is generally
* displayed above an image.
*/
val ContainerGradient = Brush.verticalGradient(
listOf(
Color(red = 28, green = 27, blue = 31, alpha = 0),
Color(red = 28, green = 27, blue = 31, alpha = 204)
)
)
/**
* Returns the content color [Color] from the colors [CardColors] for different
* interaction states.
*/
internal fun contentColor(
focused: Boolean,
pressed: Boolean,
colors: CardColors
): Color {
return when {
focused -> colors.focusedContentColor
pressed -> colors.pressedContentColor
else -> colors.contentColor
}
}
/**
* Creates a [CardShape] that represents the default container shapes used in a Card.
*
* @param shape the default shape used when the Card has no other [Interaction]s.
* @param focusedShape the shape used when the Card is focused.
* @param pressedShape the shape used when the Card is pressed.
*/
fun shape(
shape: Shape = ContainerShape,
focusedShape: Shape = shape,
pressedShape: Shape = shape
) = CardShape(
shape = shape,
focusedShape = focusedShape,
pressedShape = pressedShape
)
/**
* Creates [CardColors] that represents the default container & content colors used in a Card.
*
* @param containerColor the default container color of this Card.
* @param contentColor the default content color of this Card.
* @param focusedContainerColor the container color of this Card when focused.
* @param focusedContentColor the content color of this Card when focused.
* @param pressedContainerColor the container color of this Card when pressed.
* @param pressedContentColor the content color of this Card when pressed.
*/
@ReadOnlyComposable
@Composable
fun colors(
containerColor: Color = MaterialTheme.colorScheme.surfaceVariant,
contentColor: Color = contentColorFor(containerColor),
focusedContainerColor: Color = containerColor,
focusedContentColor: Color = contentColorFor(focusedContainerColor),
pressedContainerColor: Color = focusedContainerColor,
pressedContentColor: Color = contentColorFor(pressedContainerColor)
) = CardColors(
containerColor = containerColor,
contentColor = contentColor,
focusedContainerColor = focusedContainerColor,
focusedContentColor = focusedContentColor,
pressedContainerColor = pressedContainerColor,
pressedContentColor = pressedContentColor
)
/**
* Creates [CardColors] that represents the default colors used in a Compact Card.
*
* @param containerColor the default container color of this Card.
* @param contentColor the default content color of this Card.
* @param focusedContainerColor the container color of this Card when focused.
* @param focusedContentColor the content color of this Card when focused.
* @param pressedContainerColor the container color of this Card when pressed.
* @param pressedContentColor the content color of this Card when pressed.
*/
@ReadOnlyComposable
@Composable
fun compactCardColors(
containerColor: Color = MaterialTheme.colorScheme.surfaceVariant,
contentColor: Color = Color.White,
focusedContainerColor: Color = containerColor,
focusedContentColor: Color = contentColor,
pressedContainerColor: Color = focusedContainerColor,
pressedContentColor: Color = focusedContentColor
) = CardColors(
containerColor = containerColor,
contentColor = contentColor,
focusedContainerColor = focusedContainerColor,
focusedContentColor = focusedContentColor,
pressedContainerColor = pressedContainerColor,
pressedContentColor = pressedContentColor
)
/**
* Creates a [CardScale] that represents the default scales used in a Card.
* Scales are used to modify the size of a composable in different [Interaction] states
* e.g. 1f (original) in default state, 1.1f (scaled up) in focused state, 0.8f (scaled down)
* in pressed state, etc.
*
* @param scale the default scale to be used for this Card.
* @param focusedScale the scale to be used for this Card when focused.
* @param pressedScale the scale to be used for this Card when pressed.
*/
fun scale(
@FloatRange(from = 0.0) scale: Float = 1f,
@FloatRange(from = 0.0) focusedScale: Float = 1.1f,
@FloatRange(from = 0.0) pressedScale: Float = scale
) = CardScale(
scale = scale,
focusedScale = focusedScale,
pressedScale = pressedScale
)
/**
* Creates a [CardBorder] that represents the border [Border]s applied on a Card in
* different [Interaction] states.
*
* @param border the default [Border] to be used for this Card.
* @param focusedBorder the [Border] to be used for this Card when focused.
* @param pressedBorder the [Border] to be used for this Card when pressed.
*/
@ReadOnlyComposable
@Composable
fun border(
border: Border = Border.None,
focusedBorder: Border = Border(
border = BorderStroke(
width = 3.dp,
color = MaterialTheme.colorScheme.border
),
shape = ContainerShape
),
pressedBorder: Border = focusedBorder
) = CardBorder(
border = border,
focusedBorder = focusedBorder,
pressedBorder = pressedBorder
)
/**
* Creates a [CardGlow] that represents the default [Glow]s used in a card.
*
* @param glow the default [Glow] behind this Card.
* @param focusedGlow the [Glow] behind this Card when focused.
* @param pressedGlow the [Glow] behind this Card when pressed.
*/
fun glow(
glow: Glow = Glow.None,
focusedGlow: Glow = glow,
pressedGlow: Glow = glow
) = CardGlow(
glow = glow,
focusedGlow = focusedGlow,
pressedGlow = pressedGlow
)
}
private const val SubtitleAlpha = 0.6f
private const val DescriptionAlpha = 0.8f
@OptIn(ExperimentalTvMaterial3Api::class)
private fun CardColors.toClickableSurfaceContainerColor() =
ClickableSurfaceColor(
color = containerColor,
focusedColor = focusedContainerColor,
pressedColor = pressedContainerColor,
disabledColor = containerColor
)
@OptIn(ExperimentalTvMaterial3Api::class)
private fun CardColors.toClickableSurfaceContentColor() =
ClickableSurfaceColor(
color = contentColor,
focusedColor = focusedContentColor,
pressedColor = pressedContentColor,
disabledColor = contentColor
)
@OptIn(ExperimentalTvMaterial3Api::class)
private fun CardShape.toClickableSurfaceShape() =
ClickableSurfaceShape(
shape = shape,
focusedShape = focusedShape,
pressedShape = pressedShape,
disabledShape = shape,
focusedDisabledShape = shape
)
@OptIn(ExperimentalTvMaterial3Api::class)
private fun CardScale.toClickableSurfaceScale() =
ClickableSurfaceScale(
scale = scale,
focusedScale = focusedScale,
pressedScale = pressedScale,
disabledScale = scale,
focusedDisabledScale = scale
)
@OptIn(ExperimentalTvMaterial3Api::class)
private fun CardBorder.toClickableSurfaceBorder() =
ClickableSurfaceBorder(
border = border,
focusedBorder = focusedBorder,
pressedBorder = pressedBorder,
disabledBorder = border,
focusedDisabledBorder = border
)
@OptIn(ExperimentalTvMaterial3Api::class)
private fun CardGlow.toClickableSurfaceGlow() =
ClickableSurfaceGlow(
glow = glow,
focusedGlow = focusedGlow,
pressedGlow = pressedGlow
)