diff --git a/demo/src/main/java/com/orange/ods/demo/ui/components/lists/ComponentLists.kt b/demo/src/main/java/com/orange/ods/demo/ui/components/lists/ComponentLists.kt index ef60f2102..fabf7a841 100644 --- a/demo/src/main/java/com/orange/ods/demo/ui/components/lists/ComponentLists.kt +++ b/demo/src/main/java/com/orange/ods/demo/ui/components/lists/ComponentLists.kt @@ -33,9 +33,8 @@ import com.orange.ods.compose.component.control.OdsCheckbox import com.orange.ods.compose.component.control.OdsSwitch import com.orange.ods.compose.component.list.OdsListItem import com.orange.ods.compose.component.list.OdsListItemIcon -import com.orange.ods.compose.component.list.OdsListItemWideThumbnail -import com.orange.ods.compose.component.list.OdsListSquaredThumbnail -import com.orange.ods.compose.component.utilities.OdsImageCircleShape +import com.orange.ods.compose.component.list.OdsListItemIconType +import com.orange.ods.compose.component.list.iconType import com.orange.ods.demo.R import com.orange.ods.demo.ui.components.utilities.ComponentCustomizationBottomSheetScaffold import com.orange.ods.demo.ui.components.utilities.ComponentCustomizationChip @@ -98,7 +97,16 @@ private fun ComponentListsContent(variantListsState: VariantListsState) { variantListsState.resetTrailing() } - val modifier = Modifier.clickable {} + val iconType = when (variantListsState.selectedLeading.value) { + VariantListsState.Leading.None -> null + VariantListsState.Leading.Icon -> OdsListItemIconType.Icon + VariantListsState.Leading.CircularImage -> OdsListItemIconType.CircularImage + VariantListsState.Leading.SquareImage -> OdsListItemIconType.SquareImage + VariantListsState.Leading.WideImage -> OdsListItemIconType.WideImage + } + val modifier = with(Modifier.clickable {}) { + if (iconType != null) iconType(iconType) else this + } val text = stringResource(id = R.string.component_element_title) val secondaryText = when (variantListsState.selectedSize.value) { VariantListsState.Size.SingleLine -> null @@ -106,27 +114,23 @@ private fun ComponentListsContent(variantListsState: VariantListsState) { VariantListsState.Size.ThreeLine -> stringResource(id = R.string.component_element_lorem_ipsum) } val singleLineSecondaryText = variantListsState.selectedSize.value == VariantListsState.Size.TwoLine + val painter = when (variantListsState.selectedLeading.value) { + VariantListsState.Leading.None -> null + VariantListsState.Leading.Icon -> painterResource(id = R.drawable.ic_address_book) + VariantListsState.Leading.CircularImage, + VariantListsState.Leading.SquareImage, + VariantListsState.Leading.WideImage -> painterResource(id = R.drawable.placeholder) + } val trailing = getTrailing(variantListsState) repeat(4) { - if (variantListsState.selectedLeading.value == VariantListsState.Leading.WideImage) { - OdsListItemWideThumbnail( - modifier = modifier, - text = text, - secondaryText = secondaryText, - singleLineSecondaryText = singleLineSecondaryText, - thumbnail = painterResource(id = R.drawable.placeholder), - trailing = trailing - ) - } else { - OdsListItem( - modifier = modifier, - text = text, - secondaryText = secondaryText, - singleLineSecondaryText = singleLineSecondaryText, - icon = getLeading(variantListsState), - trailing = trailing - ) - } + OdsListItem( + modifier = modifier, + text = text, + secondaryText = secondaryText, + singleLineSecondaryText = singleLineSecondaryText, + icon = painter?.let { { OdsListItemIcon(painter = painter) } }, + trailing = trailing + ) if (variantListsState.dividerEnabled.value) { Divider(startIndent = getStartIndent(variantListsState = variantListsState)) @@ -135,17 +139,6 @@ private fun ComponentListsContent(variantListsState: VariantListsState) { } } -@ExperimentalMaterialApi -private fun getLeading(variantListsState: VariantListsState): (@Composable () -> Unit)? { - return when (variantListsState.selectedLeading.value) { - VariantListsState.Leading.None, - VariantListsState.Leading.WideImage -> null - VariantListsState.Leading.Icon -> { -> OdsListItemIcon(painter = painterResource(id = R.drawable.ic_address_book)) } - VariantListsState.Leading.CircularImage -> { -> OdsImageCircleShape(painter = painterResource(id = R.drawable.placeholder)) } - VariantListsState.Leading.SquareImage -> { -> OdsListSquaredThumbnail(painter = painterResource(id = R.drawable.placeholder)) } - } -} - @ExperimentalMaterialApi private fun getTrailing(variantListsState: VariantListsState): (@Composable () -> Unit)? { return if (variantListsState.selectedTrailing.value != VariantListsState.Trailing.None) { @@ -175,8 +168,8 @@ private fun getStartIndent(variantListsState: VariantListsState): Dp { VariantListsState.Leading.Icon, VariantListsState.Leading.CircularImage -> dimensionResource(id = R.dimen.avatar_size) + dimensionResource(id = R.dimen.spacing_m).times(2) VariantListsState.Leading.SquareImage -> { - dimensionResource(id = R.dimen.list_squared_thumbnail_size) + dimensionResource(id = R.dimen.spacing_m).times(2) + dimensionResource(id = R.dimen.list_square_image_size) + dimensionResource(id = R.dimen.spacing_m).times(2) } - VariantListsState.Leading.WideImage -> dimensionResource(id = R.dimen.list_wide_thumbnail_width) + dimensionResource(id = R.dimen.spacing_m) + VariantListsState.Leading.WideImage -> dimensionResource(id = R.dimen.list_wide_image_width) + dimensionResource(id = R.dimen.spacing_m) } } diff --git a/lib/src/main/java/com/orange/ods/compose/component/card/OdsCardTitleFirst.kt b/lib/src/main/java/com/orange/ods/compose/component/card/OdsCardTitleFirst.kt index 868ce7afe..1d1a424bd 100644 --- a/lib/src/main/java/com/orange/ods/compose/component/card/OdsCardTitleFirst.kt +++ b/lib/src/main/java/com/orange/ods/compose/component/card/OdsCardTitleFirst.kt @@ -81,7 +81,7 @@ fun OdsCardTitleFirst( Row( modifier = Modifier .fillMaxWidth() - .height(dimensionResource(id = R.dimen.list_two_line_item_with_icon_height)) + .height(dimensionResource(id = R.dimen.list_two_line_with_icon_item_height)) .padding(horizontal = dimensionResource(id = R.dimen.spacing_m)), verticalAlignment = Alignment.CenterVertically ) { diff --git a/lib/src/main/java/com/orange/ods/compose/component/list/OdsListItem.kt b/lib/src/main/java/com/orange/ods/compose/component/list/OdsListItem.kt index db5de8832..1d892d69a 100644 --- a/lib/src/main/java/com/orange/ods/compose/component/list/OdsListItem.kt +++ b/lib/src/main/java/com/orange/ods/compose/component/list/OdsListItem.kt @@ -35,7 +35,9 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import com.orange.ods.R +import com.orange.ods.compose.component.utilities.OdsImageCircleShape import com.orange.ods.compose.text.OdsTextSubtitle1 +import com.orange.ods.utilities.extension.getElementOfType import com.orange.ods.utilities.extension.isNotNullOrBlank /** @@ -45,6 +47,8 @@ import com.orange.ods.utilities.extension.isNotNullOrBlank * * To make this [OdsListItem] clickable, use [Modifier.clickable]. * + * To specify an icon type, use [Modifier.iconType] on [modifier] and call [OdsListItemScope.OdsListItemIcon] in the [icon] lambda. + * * This component can be used to achieve the list item templates existing in the spec. For example: * - one-line items * @sample androidx.compose.material.samples.OneLineListItems @@ -53,12 +57,9 @@ import com.orange.ods.utilities.extension.isNotNullOrBlank * - three-line items * @sample androidx.compose.material.samples.ThreeLineListItems * - * Note: If you want to display a big thumbnail without left margin in your items, please use [OdsListItemWideThumbnail]. - * * @param modifier Modifier to be applied to the list item * @param text The primary text of the list item * @param icon The leading supporting visual of the list item - * @param isThumbnailIcon Whether the icon is a thumbnail (more space must be allowed in this case) * @param secondaryText The secondary text of the list item * @param singleLineSecondaryText Whether the secondary text is single line * @param overlineText The text displayed above the primary text @@ -69,17 +70,61 @@ import com.orange.ods.utilities.extension.isNotNullOrBlank fun OdsListItem( modifier: Modifier = Modifier, text: String, - icon: @Composable (() -> Unit)? = null, - isThumbnailIcon: Boolean = false, + icon: @Composable (OdsListItemScope.() -> Unit)? = null, secondaryText: String? = null, singleLineSecondaryText: Boolean = true, overlineText: String? = null, - trailing: @Composable (() -> Unit)? = null + trailing: @Composable (OdsListItemScope.() -> Unit)? = null ) { + val iconType = modifier.getElementOfType()?.iconType + val listItemScope = OdsListItemScope(iconType) + if (iconType == OdsListItemIconType.WideImage) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + icon?.let { listItemScope.it() } + OdsListItemInternal( + modifier = Modifier + .weight(1f) + .iconType(OdsListItemIconType.WideImage), + listItemScope = listItemScope, + text = text, + icon = null, + secondaryText = secondaryText, + singleLineSecondaryText = singleLineSecondaryText, + overlineText = overlineText, + trailing = trailing + ) + } + } else { + OdsListItemInternal( + modifier = modifier, + listItemScope = listItemScope, + text = text, + icon = icon, + secondaryText = secondaryText, + singleLineSecondaryText = singleLineSecondaryText, + overlineText = overlineText, + trailing = trailing + ) + } +} + +@ExperimentalMaterialApi +@Composable +private fun OdsListItemInternal( + modifier: Modifier = Modifier, + listItemScope: OdsListItemScope, + text: String, + icon: @Composable (OdsListItemScope.() -> Unit)? = null, + secondaryText: String? = null, + singleLineSecondaryText: Boolean = true, + overlineText: String? = null, + trailing: @Composable (OdsListItemScope.() -> Unit)? = null +) { + val iconType = modifier.getElementOfType()?.iconType val requiredHeight = computeRequiredHeight( hasIcon = icon != null, - isThumbnailIcon = isThumbnailIcon, - hasOverline = overlineText.isNotNullOrBlank(), + iconType = iconType, + hasOverlineText = overlineText.isNotNullOrBlank(), hasText = text.isNotBlank(), hasSecondaryText = secondaryText.isNotNullOrBlank(), singleLineSecondaryText = singleLineSecondaryText @@ -89,7 +134,7 @@ fun OdsListItem( modifier = modifier .fillMaxWidth() .requiredHeight(requiredHeight), - icon = icon, + icon = icon?.let { { listItemScope.it() } }, secondaryText = if (secondaryText.isNotNullOrBlank()) { { Text(text = secondaryText, style = MaterialTheme.typography.body2, maxLines = secondaryTextLinesNumber, overflow = TextOverflow.Ellipsis) } } else null, @@ -97,7 +142,7 @@ fun OdsListItem( overlineText = if (overlineText.isNotNullOrBlank()) { { Text(text = overlineText, style = MaterialTheme.typography.overline, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)) } } else null, - trailing = trailing, + trailing = trailing?.let { { listItemScope.it() } }, text = { if (text.isNotBlank()) { OdsTextSubtitle1(text = text) @@ -107,121 +152,121 @@ fun OdsListItem( } /** - * Use this list item to display a wide thumbnail without start padding in the item, otherwise use [OdsListItem]. - * - * To make this [OdsListItemWideThumbnail] clickable, use [Modifier.clickable]. + * Displays an icon in a list item. * - * @param modifier Modifier to be applied to the list item - * @param text The primary text of the list item - * @param thumbnail The painter of the thumbnail - * @param thumbnailContentDescription The content description for the given thumbnail - * @param secondaryText The secondary text of the list item - * @param singleLineSecondaryText Whether the secondary text is single line - * @param overlineText The text displayed above the primary text - * @param trailing The trailing meta text, icon, switch or checkbox - */ -@ExperimentalMaterialApi -@Composable -fun OdsListItemWideThumbnail( - modifier: Modifier = Modifier, - text: String, - thumbnail: Painter, - thumbnailContentDescription: String? = null, - secondaryText: String? = null, - singleLineSecondaryText: Boolean = true, - overlineText: String? = null, - trailing: @Composable (() -> Unit)? = null -) { - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - OdsListWideThumbnail(painter = thumbnail, contentDescription = thumbnailContentDescription) - OdsListItem( - modifier = Modifier.weight(1f), - isThumbnailIcon = true, - text = text, - secondaryText = secondaryText, - singleLineSecondaryText = singleLineSecondaryText, - overlineText = overlineText, - trailing = trailing - ) - } -} - -/** - * Displays an icon in a list item centered vertically + * This method throws an exception if no icon type has been specified on the [OdsListItem] modifier using the [Modifier.iconType] method. * * @param painter to draw * @param contentDescription Content description of the icon */ @Composable -fun OdsListItemIcon(painter: Painter, contentDescription: String? = null) { - Column(modifier = Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) { - Icon(painter = painter, contentDescription = contentDescription) +fun OdsListItemScope.OdsListItemIcon(painter: Painter, contentDescription: String? = null) { + when (iconType) { + OdsListItemIconType.Icon -> { + Column(modifier = Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) { + Icon(painter = painter, contentDescription = contentDescription) + } + } + OdsListItemIconType.CircularImage -> { + OdsImageCircleShape(painter = painter, contentDescription = contentDescription) + } + OdsListItemIconType.SquareImage -> { + Image( + painter = painter, + contentDescription = contentDescription, + modifier = Modifier + .size(dimensionResource(id = R.dimen.list_square_image_size)) + .clip(MaterialTheme.shapes.medium), + contentScale = ContentScale.Crop + ) + } + OdsListItemIconType.WideImage -> { + Image( + painter = painter, + contentDescription = contentDescription, + contentScale = ContentScale.Crop, + modifier = Modifier + .height(dimensionResource(id = R.dimen.list_wide_image_height)) + .width(dimensionResource(id = R.dimen.list_wide_image_width)) + ) + } + null -> throw Exception("OdsListItemIcon(Painter, String?) method has been called without specifying an icon type. Please specify an icon type by calling the Modifier.iconType(OdsListItemIcon) method on the OdsList modifier.") } } -/** - * Displays a 56x100 thumbnail in a list item - * - * @param painter to draw - * @param contentDescription Content description of the icon - */ -@Composable -fun OdsListWideThumbnail(painter: Painter, contentDescription: String? = null) { - Image( - painter = painter, - contentDescription = contentDescription, - contentScale = ContentScale.Crop, - modifier = Modifier - .height(dimensionResource(id = R.dimen.list_wide_thumbnail_height)) - .width(dimensionResource(id = R.dimen.list_wide_thumbnail_width)) - ) -} - -/** - * Displays a 56x56 thumbnail in a list item - * - * @param painter to draw - * @param contentDescription Content description of the icon - */ -@Composable -fun OdsListSquaredThumbnail(painter: Painter, contentDescription: String? = null) { - Image( - painter = painter, - contentDescription = contentDescription, - modifier = Modifier - .size(dimensionResource(id = R.dimen.list_squared_thumbnail_size)) - .clip(MaterialTheme.shapes.medium), - contentScale = ContentScale.Crop - ) -} - /** * Computes the height of a list item depending on its attributes. * It allows to be able to center vertically elements in the item. */ @Composable -internal fun computeRequiredHeight( +private fun computeRequiredHeight( hasIcon: Boolean, - isThumbnailIcon: Boolean, - hasOverline: Boolean, + iconType: OdsListItemIconType?, + hasOverlineText: Boolean, hasText: Boolean, hasSecondaryText: Boolean, singleLineSecondaryText: Boolean ): Dp { + val wideImage = iconType == OdsListItemIconType.WideImage val heightRes = when { // single-line - !hasOverline && (!hasSecondaryText || !hasText) -> when { - hasIcon && !isThumbnailIcon -> R.dimen.list_single_line_with_icon_item_height - isThumbnailIcon -> R.dimen.list_single_line_with_thumbnail_item_height + !hasOverlineText && (!hasSecondaryText || !hasText) -> when { + hasIcon && !wideImage -> R.dimen.list_single_line_with_icon_item_height + wideImage -> R.dimen.list_single_line_with_wide_image_item_height else -> R.dimen.list_single_line_item_height } // three-lines - hasOverline && hasSecondaryText -> R.dimen.list_three_line_item_height + hasOverlineText && hasSecondaryText -> R.dimen.list_three_line_item_height // two-lines - hasOverline || (hasSecondaryText && singleLineSecondaryText) -> if (hasIcon || isThumbnailIcon) R.dimen.list_two_line_item_with_icon_height else R.dimen.list_two_line_item_height + hasOverlineText || (hasSecondaryText && singleLineSecondaryText) -> if (hasIcon || wideImage) R.dimen.list_two_line_with_icon_item_height else R.dimen.list_two_line_item_height // three-lines else -> R.dimen.list_three_line_item_height } return dimensionResource(id = heightRes) -} \ No newline at end of file +} + +/** + * An [OdsListItemScope] provides a scope for the children of [OdsListItem]. + * + * @param iconType The icon type + */ +data class OdsListItemScope(val iconType: OdsListItemIconType?) + +/** + * Represents the various types of icon that can be displayed in an [OdsListItem]. + */ +enum class OdsListItemIconType { + + /** A standard icon. */ + Icon, + + /** An image cropped into a circle. */ + CircularImage, + + /** An image cropped into a square. */ + SquareImage, + + /** An image cropped into a rectangle. */ + WideImage +} + +/** + * Specifies the icon type to display in an [OdsListItem]. + * + * @param iconType The icon type + */ +fun Modifier.iconType(iconType: OdsListItemIconType): Modifier { + return then(object : OdsListItemIconTypeModifier { + override val iconType: OdsListItemIconType + get() = iconType + }) +} + +/** + * A modifier that allows to configure the icon type in an [OdsListItem]. + */ +private interface OdsListItemIconTypeModifier : Modifier.Element { + + val iconType: OdsListItemIconType +} diff --git a/lib/src/main/java/com/orange/ods/utilities/extension/ModifierExt.kt b/lib/src/main/java/com/orange/ods/utilities/extension/ModifierExt.kt index 7d02398b9..ebe353d56 100644 --- a/lib/src/main/java/com/orange/ods/utilities/extension/ModifierExt.kt +++ b/lib/src/main/java/com/orange/ods/utilities/extension/ModifierExt.kt @@ -24,4 +24,16 @@ inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier interactionSource = remember { MutableInteractionSource() }) { onClick() } -} \ No newline at end of file +} + +/** + * Returns the first [Modifier.Element] of type [T] in the current modifier, or null if an element of type [T] could not be found. + * + * @param T The type of the [Modifier.Element]. + * @return The modifier element, or null if it could not be found. + */ +inline fun Modifier.getElementOfType(): T? where T : Modifier.Element { + return foldOut(null as T?) { currentElement, foundElement -> + foundElement.orElse { currentElement as? T } + } +} diff --git a/lib/src/main/res/values/ods_dimens.xml b/lib/src/main/res/values/ods_dimens.xml index 1fdb18a1c..0550eebcd 100644 --- a/lib/src/main/res/values/ods_dimens.xml +++ b/lib/src/main/res/values/ods_dimens.xml @@ -43,13 +43,13 @@ 48dp 56dp - 72dp + 72dp 64dp - 72dp + 72dp 88dp - 56dp - 100dp - 56dp + 56dp + 100dp + 56dp 8dp