/* * 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.padding import androidx.compose.material3.internal.ProvideContentColorTextStyle import androidx.compose.material3.internal.heightOrZero import androidx.compose.material3.internal.widthOrZero 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.runtime.Stable import androidx.compose.runtime.remember 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.IntrinsicMeasurable import androidx.compose.ui.layout.IntrinsicMeasureScope import androidx.compose.ui.layout.LastBaseline import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.MultiContentMeasurePolicy import androidx.compose.ui.layout.Placeable import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import androidx.compose.ui.unit.sp import kotlin.math.max /** * Material Design list item. * * Lists are continuous, vertical indexes of text or images. * * ![Lists image](https://developer.android.com/images/reference/androidx/compose/material3/lists.png) * * 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 measurePolicy = remember { ListItemMeasurePolicy() } Layout( contents = listOf( headline, overline ?: {}, supporting ?: {}, leading ?: {}, trailing ?: {}, ), measurePolicy = measurePolicy, ) } private class ListItemMeasurePolicy : MultiContentMeasurePolicy { override fun MeasureScope.measure( measurables: List>, constraints: Constraints ): MeasureResult { val (headlineMeasurable, overlineMeasurable, supportingMeasurable, leadingMeasurable, trailingMeasurable) = measurables var currentTotalWidth = 0 val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) val startPadding = ListItemStartPadding val endPadding = ListItemEndPadding val horizontalPadding = (startPadding + endPadding).roundToPx() // ListItem layout has a cycle in its dependencies which we use // intrinsic measurements to break: // 1. Intrinsic leading/trailing width // 2. Intrinsic supporting height // 3. Intrinsic vertical padding // 4. Actual leading/trailing measurement // 5. Actual supporting measurement // 6. Actual vertical padding val intrinsicLeadingWidth = leadingMeasurable.firstOrNull() ?.minIntrinsicWidth(constraints.maxHeight) ?: 0 val intrinsicTrailingWidth = trailingMeasurable.firstOrNull() ?.minIntrinsicWidth(constraints.maxHeight) ?: 0 val intrinsicSupportingWidthConstraint = looseConstraints.maxWidth .subtractConstraintSafely( intrinsicLeadingWidth + intrinsicTrailingWidth + horizontalPadding ) val intrinsicSupportingHeight = supportingMeasurable.firstOrNull() ?.minIntrinsicHeight(intrinsicSupportingWidthConstraint) ?: 0 val intrinsicIsSupportingMultiline = isSupportingMultilineHeuristic(intrinsicSupportingHeight) val intrinsicListItemType = ListItemType( hasOverline = overlineMeasurable.firstOrNull() != null, hasSupporting = supportingMeasurable.firstOrNull() != null, isSupportingMultiline = intrinsicIsSupportingMultiline, ) val intrinsicVerticalPadding = (verticalPadding(intrinsicListItemType) * 2).roundToPx() val paddedLooseConstraints = looseConstraints.offset( horizontal = -horizontalPadding, vertical = -intrinsicVerticalPadding, ) val leadingPlaceable = leadingMeasurable.firstOrNull()?.measure(paddedLooseConstraints) currentTotalWidth += widthOrZero(leadingPlaceable) val trailingPlaceable = trailingMeasurable.firstOrNull()?.measure( paddedLooseConstraints.offset( horizontal = -currentTotalWidth ) ) currentTotalWidth += widthOrZero(trailingPlaceable) var currentTotalHeight = 0 val headlinePlaceable = headlineMeasurable.firstOrNull()?.measure( paddedLooseConstraints.offset( horizontal = -currentTotalWidth ) ) currentTotalHeight += heightOrZero(headlinePlaceable) val supportingPlaceable = supportingMeasurable.firstOrNull()?.measure( paddedLooseConstraints.offset( horizontal = -currentTotalWidth, vertical = -currentTotalHeight ) ) currentTotalHeight += heightOrZero(supportingPlaceable) val isSupportingMultiline = supportingPlaceable != null && (supportingPlaceable[FirstBaseline] != supportingPlaceable[LastBaseline]) val overlinePlaceable = overlineMeasurable.firstOrNull()?.measure( paddedLooseConstraints.offset( horizontal = -currentTotalWidth, vertical = -currentTotalHeight ) ) val listItemType = ListItemType( hasOverline = overlinePlaceable != null, hasSupporting = supportingPlaceable != null, isSupportingMultiline = isSupportingMultiline, ) val topPadding = verticalPadding(listItemType) val verticalPadding = topPadding * 2 val width = calculateWidth( leadingWidth = widthOrZero(leadingPlaceable), trailingWidth = widthOrZero(trailingPlaceable), headlineWidth = widthOrZero(headlinePlaceable), overlineWidth = widthOrZero(overlinePlaceable), supportingWidth = widthOrZero(supportingPlaceable), horizontalPadding = horizontalPadding, constraints = constraints, ) val height = calculateHeight( leadingHeight = heightOrZero(leadingPlaceable), trailingHeight = heightOrZero(trailingPlaceable), headlineHeight = heightOrZero(headlinePlaceable), overlineHeight = heightOrZero(overlinePlaceable), supportingHeight = heightOrZero(supportingPlaceable), listItemType = listItemType, verticalPadding = verticalPadding.roundToPx(), constraints = constraints, ) return place( width = width, height = height, leadingPlaceable = leadingPlaceable, trailingPlaceable = trailingPlaceable, headlinePlaceable = headlinePlaceable, overlinePlaceable = overlinePlaceable, supportingPlaceable = supportingPlaceable, isThreeLine = listItemType == ListItemType.ThreeLine, startPadding = startPadding.roundToPx(), endPadding = endPadding.roundToPx(), topPadding = topPadding.roundToPx(), ) } override fun IntrinsicMeasureScope.maxIntrinsicHeight( measurables: List>, width: Int ): Int = calculateIntrinsicHeight(measurables, width, IntrinsicMeasurable::maxIntrinsicHeight) override fun IntrinsicMeasureScope.maxIntrinsicWidth( measurables: List>, height: Int ): Int = calculateIntrinsicWidth(measurables, height, IntrinsicMeasurable::maxIntrinsicWidth) override fun IntrinsicMeasureScope.minIntrinsicHeight( measurables: List>, width: Int ): Int = calculateIntrinsicHeight(measurables, width, IntrinsicMeasurable::minIntrinsicHeight) override fun IntrinsicMeasureScope.minIntrinsicWidth( measurables: List>, height: Int ): Int = calculateIntrinsicWidth(measurables, height, IntrinsicMeasurable::minIntrinsicWidth) private fun IntrinsicMeasureScope.calculateIntrinsicWidth( measurables: List>, height: Int, intrinsicMeasure: IntrinsicMeasurable.(height: Int) -> Int, ): Int { val (headlineMeasurable, overlineMeasurable, supportingMeasurable, leadingMeasurable, trailingMeasurable) = measurables return calculateWidth( leadingWidth = leadingMeasurable.firstOrNull()?.intrinsicMeasure(height) ?: 0, trailingWidth = trailingMeasurable.firstOrNull()?.intrinsicMeasure(height) ?: 0, headlineWidth = headlineMeasurable.firstOrNull()?.intrinsicMeasure(height) ?: 0, overlineWidth = overlineMeasurable.firstOrNull()?.intrinsicMeasure(height) ?: 0, supportingWidth = supportingMeasurable.firstOrNull()?.intrinsicMeasure(height) ?: 0, horizontalPadding = (ListItemStartPadding + ListItemEndPadding).roundToPx(), constraints = Constraints(), ) } private fun IntrinsicMeasureScope.calculateIntrinsicHeight( measurables: List>, width: Int, intrinsicMeasure: IntrinsicMeasurable.(width: Int) -> Int, ): Int { val (headlineMeasurable, overlineMeasurable, supportingMeasurable, leadingMeasurable, trailingMeasurable) = measurables var remainingWidth = width.subtractConstraintSafely( (ListItemStartPadding + ListItemEndPadding).roundToPx() ) val leadingHeight = leadingMeasurable.firstOrNull()?.let { val height = it.intrinsicMeasure(remainingWidth) remainingWidth = remainingWidth.subtractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) height } ?: 0 val trailingHeight = trailingMeasurable.firstOrNull()?.let { val height = it.intrinsicMeasure(remainingWidth) remainingWidth = remainingWidth.subtractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) height } ?: 0 val overlineHeight = overlineMeasurable.firstOrNull() ?.intrinsicMeasure(remainingWidth) ?: 0 val supportingHeight = supportingMeasurable.firstOrNull() ?.intrinsicMeasure(remainingWidth) ?: 0 val isSupportingMultiline = isSupportingMultilineHeuristic(supportingHeight) val listItemType = ListItemType( hasOverline = overlineHeight > 0, hasSupporting = supportingHeight > 0, isSupportingMultiline = isSupportingMultiline, ) return calculateHeight( leadingHeight = leadingHeight, trailingHeight = trailingHeight, headlineHeight = headlineMeasurable.firstOrNull()?.intrinsicMeasure(width) ?: 0, overlineHeight = overlineHeight, supportingHeight = supportingHeight, listItemType = listItemType, verticalPadding = (verticalPadding(listItemType) * 2).roundToPx(), constraints = Constraints(), ) } } private fun IntrinsicMeasureScope.calculateWidth( leadingWidth: Int, trailingWidth: Int, headlineWidth: Int, overlineWidth: Int, supportingWidth: Int, horizontalPadding: Int, constraints: Constraints, ): Int { if (constraints.hasBoundedWidth) { return constraints.maxWidth } // Fallback behavior if width constraints are infinite val mainContentWidth = maxOf(headlineWidth, overlineWidth, supportingWidth) return horizontalPadding + leadingWidth + mainContentWidth + trailingWidth } private fun IntrinsicMeasureScope.calculateHeight( leadingHeight: Int, trailingHeight: Int, headlineHeight: Int, overlineHeight: Int, supportingHeight: Int, listItemType: ListItemType, verticalPadding: Int, 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 mainContentHeight = headlineHeight + overlineHeight + supportingHeight return max( minHeight, verticalPadding + maxOf(leadingHeight, mainContentHeight, trailingHeight) ).coerceAtMost(constraints.maxHeight) } private fun MeasureScope.place( width: Int, height: Int, leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, headlinePlaceable: Placeable?, overlinePlaceable: Placeable?, supportingPlaceable: Placeable?, isThreeLine: Boolean, startPadding: Int, endPadding: Int, topPadding: Int, ): MeasureResult { return layout(width, height) { 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 */ @Stable internal fun headlineColor(enabled: Boolean): Color { return if (enabled) headlineColor else disabledHeadlineColor } /** The color of this [ListItem]'s leading content based on enabled state */ @Stable internal fun leadingIconColor(enabled: Boolean): Color = if (enabled) leadingIconColor else disabledLeadingIconColor /** The color of this [ListItem]'s overline text based on enabled state */ @Stable internal fun overlineColor(): Color = overlineColor /** The color of this [ListItem]'s supporting text based on enabled state */ @Stable internal fun supportingColor(): Color = supportingTextColor /** The color of this [ListItem]'s trailing content based on enabled state */ @Stable 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 operator fun invoke( 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 = 16.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 // In the actual layout phase, we can query supporting baselines, // but for an intrinsic measurement pass, we have to estimate. private fun Density.isSupportingMultilineHeuristic( estimatedSupportingHeight: Int ): Boolean = estimatedSupportingHeight > 30.sp.roundToPx() private fun verticalPadding(listItemType: ListItemType): Dp = when (listItemType) { ListItemType.ThreeLine -> ListItemThreeLineVerticalPadding else -> ListItemVerticalPadding } private fun Int.subtractConstraintSafely(n: Int): Int { if (this == Constraints.Infinity) { return this } return this - n }