blob: 92aa5814bf18f61a3362f0f5402ab4a72ec094ab [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.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
/**
* [StandardCardLayout] is an opinionated TV Material Card layout with an image and text content
* to show information about a subject.
*
* It provides a vertical layout with an image card slot at the top. And below that, there are
* slots for the title, subtitle and description.
*
* @param imageCard defines the [Composable] to be used for the image card. See
* [CardLayoutDefaults.ImageCard] to create an image card. The `interactionSource` param provided
* in the lambda function should be forwarded and used with the image card composable.
* @param title defines the [Composable] title placed below the image card in the CardLayout.
* @param modifier the [Modifier] to be applied to this CardLayout.
* @param subtitle defines the [Composable] supporting text placed below the title in CardLayout.
* @param description defines the [Composable] description placed below the subtitle in CardLayout.
* @param contentColor [CardLayoutColors] defines the content color used in the CardLayout
* for different interaction states. See [CardLayoutDefaults.contentColor].
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this CardLayout. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this card layout in different states.
* This interaction source param would also be forwarded to be used with the `imageCard` composable.
*/
@ExperimentalTvMaterial3Api
@Composable
fun StandardCardLayout(
imageCard: @Composable (interactionSource: MutableInteractionSource) -> Unit,
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
subtitle: @Composable () -> Unit = {},
description: @Composable () -> Unit = {},
contentColor: CardLayoutColors = CardLayoutDefaults.contentColor(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
val focused by interactionSource.collectIsFocusedAsState()
val pressed by interactionSource.collectIsPressedAsState()
Column(
modifier = modifier
) {
Box(
contentAlignment = CardDefaults.ContentImageAlignment,
) {
imageCard(interactionSource)
}
Column(
modifier = Modifier
.align(Alignment.CenterHorizontally),
horizontalAlignment = Alignment.CenterHorizontally
) {
CardLayoutContent(
title = title,
subtitle = subtitle,
description = description,
contentColor = contentColor.color(
focused = focused,
pressed = pressed
)
)
}
}
}
/**
* [WideCardLayout] is an opinionated TV Material Card layout with an image and text content
* to show information about a subject.
*
* It provides a horizontal layout with an image card slot at the start, followed by the title,
* subtitle and description at the end.
*
* @param imageCard defines the [Composable] to be used for the image card. See
* [CardLayoutDefaults.ImageCard] to create an image card. The `interactionSource` param provided
* in the lambda function should to be forwarded and used with the image card composable.
* @param title defines the [Composable] title placed below the image card in the CardLayout.
* @param modifier the [Modifier] to be applied to this CardLayout.
* @param subtitle defines the [Composable] supporting text placed below the title in CardLayout.
* @param description defines the [Composable] description placed below the subtitle in CardLayout.
* @param contentColor [CardLayoutColors] defines the content color used in the CardLayout
* for different interaction states. See [CardLayoutDefaults.contentColor].
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this CardLayout. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this card layout in different states.
* This interaction source param would also be forwarded to be used with the `imageCard` composable.
*/
@ExperimentalTvMaterial3Api
@Composable
fun WideCardLayout(
imageCard: @Composable (interactionSource: MutableInteractionSource) -> Unit,
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
subtitle: @Composable () -> Unit = {},
description: @Composable () -> Unit = {},
contentColor: CardLayoutColors = CardLayoutDefaults.contentColor(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
val focused by interactionSource.collectIsFocusedAsState()
val pressed by interactionSource.collectIsPressedAsState()
Row(
modifier = modifier
) {
Box(
contentAlignment = CardDefaults.ContentImageAlignment
) {
imageCard(interactionSource)
}
Column {
CardLayoutContent(
title = title,
subtitle = subtitle,
description = description,
contentColor = contentColor.color(
focused = focused,
pressed = pressed
)
)
}
}
}
@Composable
internal fun CardLayoutContent(
title: @Composable () -> Unit,
subtitle: @Composable () -> Unit = {},
description: @Composable () -> Unit = {},
contentColor: Color
) {
CompositionLocalProvider(LocalContentColor provides contentColor) {
CardContent(title, subtitle, description)
}
}
@ExperimentalTvMaterial3Api
object CardLayoutDefaults {
/**
* Creates [CardLayoutColors] that represents the default content colors used in a
* CardLayout.
*
* @param contentColor the default content color of this CardLayout.
* @param focusedContentColor the content color of this CardLayout when focused.
* @param pressedContentColor the content color of this CardLayout when pressed.
*/
@ReadOnlyComposable
@Composable
fun contentColor(
contentColor: Color = MaterialTheme.colorScheme.onSurface,
focusedContentColor: Color = contentColor,
pressedContentColor: Color = focusedContentColor
) = CardLayoutColors(
contentColor = contentColor,
focusedContentColor = focusedContentColor,
pressedContentColor = pressedContentColor
)
/**
* [ImageCard] is basically a [Card] composable with an image as the content. It is recommended
* to be used with the different CardLayout(s).
*
* This Card handles click events, calling its [onClick] lambda.
*
* @param onClick called when this card is clicked
* @param interactionSource the [MutableInteractionSource] representing the stream of
* [Interaction]s for this card. When using with the CardLayout(s), it is recommended to
* pass in the interaction state obtained from the parent lambda.
* @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 content defines the image content [Composable] to be displayed inside the Card.
*/
@Composable
fun ImageCard(
onClick: () -> Unit,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
shape: CardShape = CardDefaults.shape(),
colors: CardColors = CardDefaults.colors(),
scale: CardScale = CardDefaults.scale(),
border: CardBorder = CardDefaults.border(),
glow: CardGlow = CardDefaults.glow(),
content: @Composable () -> Unit
) {
Card(
onClick = onClick,
modifier = modifier,
shape = shape,
colors = colors,
scale = scale,
border = border,
glow = glow,
interactionSource = interactionSource
) {
content()
}
}
}
/**
* Represents the [Color] of content in a CardLayout for different interaction states.
*/
@ExperimentalTvMaterial3Api
@Immutable
class CardLayoutColors internal constructor(
internal val contentColor: Color,
internal val focusedContentColor: Color,
internal val pressedContentColor: Color,
) {
/**
* Returns the content color [Color] for different interaction states.
*/
internal fun color(
focused: Boolean,
pressed: Boolean
): Color {
return when {
focused -> focusedContentColor
pressed -> pressedContentColor
else -> contentColor
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as CardLayoutColors
if (contentColor != other.contentColor) return false
if (focusedContentColor != other.focusedContentColor) return false
if (pressedContentColor != other.pressedContentColor) return false
return true
}
override fun hashCode(): Int {
var result = contentColor.hashCode()
result = 31 * result + focusedContentColor.hashCode()
result = 31 * result + pressedContentColor.hashCode()
return result
}
override fun toString(): String {
return "CardLayoutContentColor(" +
"contentColor=$contentColor, " +
"focusedContentColor=$focusedContentColor, " +
"pressedContentColor=$pressedContentColor)"
}
}