/*
* 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.annotation.VisibleForTesting
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.tokens.ListTokens
import androidx.compose.material3.tokens.TypographyKeyTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.layout.LastBaseline
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import kotlin.math.max
/**
* Material Design list item.
*
* Lists are continuous, vertical indexes of text or images.
*
* 
*
* This component can be used to achieve the list item templates existing in the spec. One-line list
* items have a singular line of headline content. Two-line list items additionally have either
* supporting or overline content. Three-line list items have either both supporting and overline
* content, or extended (two-line) supporting text. For example:
* - one-line item
* @sample androidx.compose.material3.samples.OneLineListItem
* - two-line item
* @sample androidx.compose.material3.samples.TwoLineListItem
* - three-line item with both overline and supporting content
* @sample androidx.compose.material3.samples.ThreeLineListItemWithOverlineAndSupporting
* - three-line item with extended supporting content
* @sample androidx.compose.material3.samples.ThreeLineListItemWithExtendedSupporting
*
* @param headlineContent the headline content of the list item
* @param modifier [Modifier] to be applied to the list item
* @param overlineContent the content displayed above the headline content
* @param supportingContent the supporting content of the list item
* @param leadingContent the leading content of the list item
* @param trailingContent the trailing meta text, icon, switch or checkbox
* @param colors [ListItemColors] that will be used to resolve the background and content color for
* this list item in different states. See [ListItemDefaults.colors]
* @param tonalElevation the tonal elevation of this list item
* @param shadowElevation the shadow elevation of this list item
*/
@Composable
fun ListItem(
headlineContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
overlineContent: @Composable (() -> Unit)? = null,
supportingContent: @Composable (() -> Unit)? = null,
leadingContent: @Composable (() -> Unit)? = null,
trailingContent: @Composable (() -> Unit)? = null,
colors: ListItemColors = ListItemDefaults.colors(),
tonalElevation: Dp = ListItemDefaults.Elevation,
shadowElevation: Dp = ListItemDefaults.Elevation,
) {
val decoratedHeadlineContent: @Composable () -> Unit = {
ProvideTextStyleFromToken(
colors.headlineColor(enabled = true),
ListTokens.ListItemLabelTextFont,
headlineContent
)
}
val decoratedSupportingContent: @Composable (() -> Unit)? = supportingContent?.let {
{
ProvideTextStyleFromToken(
colors.supportingColor(),
ListTokens.ListItemSupportingTextFont,
it
)
}
}
val decoratedOverlineContent: @Composable (() -> Unit)? = overlineContent?.let {
{
ProvideTextStyleFromToken(
colors.overlineColor(),
ListTokens.ListItemOverlineFont,
it
)
}
}
val decoratedLeadingContent: @Composable (() -> Unit)? = leadingContent?.let {
{
Box(Modifier.padding(end = LeadingContentEndPadding)) {
CompositionLocalProvider(
LocalContentColor provides colors.leadingIconColor(enabled = true),
content = it
)
}
}
}
val decoratedTrailingContent: @Composable (() -> Unit)? = trailingContent?.let {
{
Box(Modifier.padding(start = TrailingContentStartPadding)) {
ProvideTextStyleFromToken(
colors.trailingIconColor(enabled = true),
ListTokens.ListItemTrailingSupportingTextFont,
content = it
)
}
}
}
Surface(
modifier = Modifier
.semantics(mergeDescendants = true) {}
.then(modifier),
shape = ListItemDefaults.shape,
color = colors.containerColor(),
contentColor = colors.headlineColor(enabled = true),
tonalElevation = tonalElevation,
shadowElevation = shadowElevation,
) {
ListItemLayout(
headline = decoratedHeadlineContent,
overline = decoratedOverlineContent,
supporting = decoratedSupportingContent,
leading = decoratedLeadingContent,
trailing = decoratedTrailingContent,
)
}
}
@Composable
private fun ListItemLayout(
leading: @Composable (() -> Unit)?,
trailing: @Composable (() -> Unit)?,
headline: @Composable () -> Unit,
overline: @Composable (() -> Unit)?,
supporting: @Composable (() -> Unit)?,
) {
val layoutDirection = LocalLayoutDirection.current
Layout(
contents = listOf(
headline,
overline ?: {},
supporting ?: {},
leading ?: {},
trailing ?: {},
)
) { measurables, constraints ->
val (headlineMeasurable, overlineMeasurable, supportingMeasurable, leadingMeasurable,
trailingMeasurable) = measurables
var currentTotalWidth = 0
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
.offset(
horizontal = -(ListItemStartPadding + ListItemEndPadding).roundToPx(),
vertical = -(ListItemVerticalPadding * 2).roundToPx()
)
val leadingPlaceable = leadingMeasurable.firstOrNull()?.measure(looseConstraints)
currentTotalWidth += widthOrZero(leadingPlaceable)
val trailingPlaceable = trailingMeasurable.firstOrNull()?.measure(
looseConstraints.offset(
horizontal = -currentTotalWidth
)
)
currentTotalWidth += widthOrZero(trailingPlaceable)
var currentTotalHeight = 0
val headlinePlaceable = headlineMeasurable.firstOrNull()?.measure(
looseConstraints.offset(
horizontal = -currentTotalWidth
)
)
currentTotalHeight += heightOrZero(headlinePlaceable)
val supportingPlaceable = supportingMeasurable.firstOrNull()?.measure(
looseConstraints.offset(
horizontal = -currentTotalWidth,
vertical = -currentTotalHeight
)
)
currentTotalHeight += heightOrZero(supportingPlaceable)
val isSupportingMultiline = supportingPlaceable != null &&
(supportingPlaceable[FirstBaseline] != supportingPlaceable[LastBaseline])
val overlinePlaceable = overlineMeasurable.firstOrNull()?.measure(
looseConstraints.offset(
horizontal = -currentTotalWidth,
vertical = -currentTotalHeight
)
)
val listItemType = ListItemType.getListItemType(
hasOverline = overlinePlaceable != null,
hasSupporting = supportingPlaceable != null,
isSupportingMultiline = isSupportingMultiline
)
val isThreeLine = listItemType == ListItemType.ThreeLine
val paddingValues = PaddingValues(
start = ListItemStartPadding,
end = ListItemEndPadding,
top = if (isThreeLine) ListItemThreeLineVerticalPadding else ListItemVerticalPadding,
bottom = if (isThreeLine) ListItemThreeLineVerticalPadding else ListItemVerticalPadding,
)
val width = calculateWidth(
leadingPlaceable = leadingPlaceable,
trailingPlaceable = trailingPlaceable,
headlinePlaceable = headlinePlaceable,
overlinePlaceable = overlinePlaceable,
supportingPlaceable = supportingPlaceable,
paddingValues = paddingValues,
layoutDirection = layoutDirection,
constraints = constraints,
)
val height = calculateHeight(
leadingPlaceable = leadingPlaceable,
trailingPlaceable = trailingPlaceable,
headlinePlaceable = headlinePlaceable,
overlinePlaceable = overlinePlaceable,
supportingPlaceable = supportingPlaceable,
listItemType = listItemType,
paddingValues = paddingValues,
constraints = constraints,
)
place(
width = width,
height = height,
leadingPlaceable = leadingPlaceable,
trailingPlaceable = trailingPlaceable,
headlinePlaceable = headlinePlaceable,
overlinePlaceable = overlinePlaceable,
supportingPlaceable = supportingPlaceable,
isThreeLine = isThreeLine,
layoutDirection = layoutDirection,
paddingValues = paddingValues,
)
}
}
private fun MeasureScope.calculateWidth(
leadingPlaceable: Placeable?,
trailingPlaceable: Placeable?,
headlinePlaceable: Placeable?,
overlinePlaceable: Placeable?,
supportingPlaceable: Placeable?,
layoutDirection: LayoutDirection,
paddingValues: PaddingValues,
constraints: Constraints,
): Int {
if (constraints.hasBoundedWidth) {
return constraints.maxWidth
}
// Fallback behavior if width constraints are infinite
val horizontalPadding = (paddingValues.calculateLeftPadding(layoutDirection) +
paddingValues.calculateRightPadding(layoutDirection)).roundToPx()
val mainContentWidth = maxOf(
widthOrZero(headlinePlaceable),
widthOrZero(overlinePlaceable),
widthOrZero(supportingPlaceable),
)
return horizontalPadding +
widthOrZero(leadingPlaceable) +
mainContentWidth +
widthOrZero(trailingPlaceable)
}
private fun MeasureScope.calculateHeight(
leadingPlaceable: Placeable?,
trailingPlaceable: Placeable?,
headlinePlaceable: Placeable?,
overlinePlaceable: Placeable?,
supportingPlaceable: Placeable?,
listItemType: ListItemType,
paddingValues: PaddingValues,
constraints: Constraints,
): Int {
val defaultMinHeight = when (listItemType) {
ListItemType.OneLine -> ListTokens.ListItemOneLineContainerHeight
ListItemType.TwoLine -> ListTokens.ListItemTwoLineContainerHeight
else /* ListItemType.ThreeLine */ -> ListTokens.ListItemThreeLineContainerHeight
}
val minHeight = max(constraints.minHeight, defaultMinHeight.roundToPx())
val verticalPadding =
paddingValues.calculateTopPadding() + paddingValues.calculateBottomPadding()
val mainContentHeight = heightOrZero(headlinePlaceable) +
heightOrZero(overlinePlaceable) +
heightOrZero(supportingPlaceable)
return max(
minHeight,
verticalPadding.roundToPx() + maxOf(
heightOrZero(leadingPlaceable),
mainContentHeight,
heightOrZero(trailingPlaceable),
)
).coerceAtMost(constraints.maxHeight)
}
private fun MeasureScope.place(
width: Int,
height: Int,
leadingPlaceable: Placeable?,
trailingPlaceable: Placeable?,
headlinePlaceable: Placeable?,
overlinePlaceable: Placeable?,
supportingPlaceable: Placeable?,
isThreeLine: Boolean,
layoutDirection: LayoutDirection,
paddingValues: PaddingValues,
): MeasureResult {
return layout(width, height) {
val startPadding = paddingValues.calculateStartPadding(layoutDirection).roundToPx()
val endPadding = paddingValues.calculateEndPadding(layoutDirection).roundToPx()
val topPadding = paddingValues.calculateTopPadding().roundToPx()
leadingPlaceable?.let {
it.placeRelative(
x = startPadding,
y = if (isThreeLine) topPadding else CenterVertically.align(it.height, height)
)
}
trailingPlaceable?.let {
it.placeRelative(
x = width - endPadding - it.width,
y = if (isThreeLine) topPadding else CenterVertically.align(it.height, height)
)
}
val mainContentX = startPadding + widthOrZero(leadingPlaceable)
val mainContentY = if (isThreeLine) {
topPadding
} else {
val totalHeight = heightOrZero(headlinePlaceable) + heightOrZero(overlinePlaceable) +
heightOrZero(supportingPlaceable)
CenterVertically.align(totalHeight, height)
}
var currentY = mainContentY
overlinePlaceable?.placeRelative(mainContentX, currentY)
currentY += heightOrZero(overlinePlaceable)
headlinePlaceable?.placeRelative(mainContentX, currentY)
currentY += heightOrZero(headlinePlaceable)
supportingPlaceable?.placeRelative(mainContentX, currentY)
}
}
/**
* Contains the default values used by list items.
*/
object ListItemDefaults {
/** The default elevation of a list item */
val Elevation: Dp = ListTokens.ListItemContainerElevation
/** The default shape of a list item */
val shape: Shape
@Composable
@ReadOnlyComposable get() = ListTokens.ListItemContainerShape.value
/** The container color of a list item */
val containerColor: Color
@Composable
@ReadOnlyComposable get() = ListTokens.ListItemContainerColor.value
/** The content color of a list item */
val contentColor: Color
@Composable
@ReadOnlyComposable get() = ListTokens.ListItemLabelTextColor.value
/**
* Creates a [ListItemColors] that represents the default container and content colors used in a
* [ListItem].
*
* @param containerColor the container color of this list item when enabled.
* @param headlineColor the headline text content color of this list item when
* enabled.
* @param leadingIconColor the color of this list item's leading content when enabled.
* @param overlineColor the overline text color of this list item
* @param supportingColor the supporting text color of this list item
* @param trailingIconColor the color of this list item's trailing content when enabled.
* @param disabledHeadlineColor the content color of this list item when not enabled.
* @param disabledLeadingIconColor the color of this list item's leading content when not
* enabled.
* @param disabledTrailingIconColor the color of this list item's trailing content when not
* enabled.
*/
@Composable
fun colors(
containerColor: Color = ListTokens.ListItemContainerColor.value,
headlineColor: Color = ListTokens.ListItemLabelTextColor.value,
leadingIconColor: Color = ListTokens.ListItemLeadingIconColor.value,
overlineColor: Color = ListTokens.ListItemOverlineColor.value,
supportingColor: Color = ListTokens.ListItemSupportingTextColor.value,
trailingIconColor: Color = ListTokens.ListItemTrailingIconColor.value,
disabledHeadlineColor: Color = ListTokens.ListItemDisabledLabelTextColor.value
.copy(alpha = ListTokens.ListItemDisabledLabelTextOpacity),
disabledLeadingIconColor: Color = ListTokens.ListItemDisabledLeadingIconColor.value
.copy(alpha = ListTokens.ListItemDisabledLeadingIconOpacity),
disabledTrailingIconColor: Color = ListTokens.ListItemDisabledTrailingIconColor.value
.copy(alpha = ListTokens.ListItemDisabledTrailingIconOpacity)
): ListItemColors =
ListItemColors(
containerColor = containerColor,
headlineColor = headlineColor,
leadingIconColor = leadingIconColor,
overlineColor = overlineColor,
supportingTextColor = supportingColor,
trailingIconColor = trailingIconColor,
disabledHeadlineColor = disabledHeadlineColor,
disabledLeadingIconColor = disabledLeadingIconColor,
disabledTrailingIconColor = disabledTrailingIconColor,
)
}
/**
* Represents the container and content colors used in a list item in different states.
*
* @constructor create an instance with arbitrary colors.
* See [ListItemDefaults.colors] for the default colors used in a [ListItem].
*
* @param containerColor the container color of this list item when enabled.
* @param headlineColor the headline text content color of this list item when
* enabled.
* @param leadingIconColor the color of this list item's leading content when enabled.
* @param overlineColor the overline text color of this list item
* @param supportingTextColor the supporting text color of this list item
* @param trailingIconColor the color of this list item's trailing content when enabled.
* @param disabledHeadlineColor the content color of this list item when not enabled.
* @param disabledLeadingIconColor the color of this list item's leading content when not
* enabled.
* @param disabledTrailingIconColor the color of this list item's trailing content when not
* enabled.
*/
@Immutable
class ListItemColors constructor(
val containerColor: Color,
val headlineColor: Color,
val leadingIconColor: Color,
val overlineColor: Color,
val supportingTextColor: Color,
val trailingIconColor: Color,
val disabledHeadlineColor: Color,
val disabledLeadingIconColor: Color,
val disabledTrailingIconColor: Color,
) {
/** The container color of this [ListItem] based on enabled state */
internal fun containerColor(): Color {
return containerColor
}
/** The color of this [ListItem]'s headline text based on enabled state */
internal fun headlineColor(enabled: Boolean): Color {
return if (enabled) headlineColor else disabledHeadlineColor
}
/** The color of this [ListItem]'s leading content based on enabled state */
internal fun leadingIconColor(enabled: Boolean): Color =
if (enabled) leadingIconColor else disabledLeadingIconColor
/** The color of this [ListItem]'s overline text based on enabled state */
internal fun overlineColor(): Color = overlineColor
/** The color of this [ListItem]'s supporting text based on enabled state */
internal fun supportingColor(): Color = supportingTextColor
/** The color of this [ListItem]'s trailing content based on enabled state */
internal fun trailingIconColor(enabled: Boolean): Color =
if (enabled) trailingIconColor else disabledTrailingIconColor
}
@Composable
private fun ProvideTextStyleFromToken(
color: Color,
textToken: TypographyKeyTokens,
content: @Composable () -> Unit,
) = ProvideContentColorTextStyle(
contentColor = color,
textStyle = MaterialTheme.typography.fromToken(textToken),
content = content
)
/**
* Helper class to define list item type. Used for padding and sizing definition.
*/
@JvmInline
private value class ListItemType private constructor(private val lines: Int) :
Comparable {
override operator fun compareTo(other: ListItemType) = lines.compareTo(other.lines)
companion object {
/** One line list item */
val OneLine = ListItemType(1)
/** Two line list item */
val TwoLine = ListItemType(2)
/** Three line list item */
val ThreeLine = ListItemType(3)
internal fun getListItemType(
hasOverline: Boolean,
hasSupporting: Boolean,
isSupportingMultiline: Boolean
): ListItemType {
return when {
(hasOverline && hasSupporting) || isSupportingMultiline -> ThreeLine
hasOverline || hasSupporting -> TwoLine
else -> OneLine
}
}
}
}
// Container related defaults
// TODO: Make sure these values stay up to date until replaced with tokens.
@VisibleForTesting
internal val ListItemVerticalPadding = 8.dp
@VisibleForTesting
internal val ListItemThreeLineVerticalPadding = 12.dp
@VisibleForTesting
internal val ListItemStartPadding = 16.dp
@VisibleForTesting
internal val ListItemEndPadding = 24.dp
// Icon related defaults.
// TODO: Make sure these values stay up to date until replaced with tokens.
@VisibleForTesting
internal val LeadingContentEndPadding = 16.dp
// Trailing related defaults.
// TODO: Make sure these values stay up to date until replaced with tokens.
@VisibleForTesting
internal val TrailingContentStartPadding = 16.dp