diff --git a/catalog/src/main/java/com/telefonica/mistica/catalog/ui/compose/components/MediaCards.kt b/catalog/src/main/java/com/telefonica/mistica/catalog/ui/compose/components/MediaCards.kt index 5b9dd1589..69184f43f 100644 --- a/catalog/src/main/java/com/telefonica/mistica/catalog/ui/compose/components/MediaCards.kt +++ b/catalog/src/main/java/com/telefonica/mistica/catalog/ui/compose/components/MediaCards.kt @@ -3,11 +3,14 @@ package com.telefonica.mistica.catalog.ui.compose.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button as MaterialButton import androidx.compose.material.Checkbox import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem @@ -25,6 +28,7 @@ import com.telefonica.mistica.catalog.R import com.telefonica.mistica.compose.card.Action import com.telefonica.mistica.compose.card.mediacard.MediaCard import com.telefonica.mistica.compose.card.mediacard.MediaCardImage.MediaCardImageResource +import com.telefonica.mistica.compose.card.mediacard.MediaCardImagePosition import com.telefonica.mistica.compose.tag.Tag import com.telefonica.mistica.tag.TagView.Companion.TYPE_PROMO @@ -53,6 +57,9 @@ fun MediaCards() { var withAdditionalContent: Boolean by remember { mutableStateOf(false) } + var imagePosition: MediaCardImagePosition by remember { mutableStateOf(MediaCardImagePosition.Top) } + var imagePositionExpanded: Boolean by remember { mutableStateOf(false) } + Column( modifier = Modifier .fillMaxSize() @@ -63,6 +70,11 @@ fun MediaCards() { ) { OutlinedTextField(value = tag, onValueChange = { tag = it }, label = { Text("Tag") }) + MaterialButton( + onClick = { tagTypeDropDownExpanded = true } + ) { + Text("Select Tag Type: ${tagTypeItems[tagType]}") + } DropdownMenu(expanded = tagTypeDropDownExpanded, onDismissRequest = { tagTypeDropDownExpanded = false }) { tagTypeItems.forEachIndexed { index, s -> DropdownMenuItem(onClick = { @@ -89,6 +101,23 @@ fun MediaCards() { Checkbox(checked = withAdditionalContent, onCheckedChange = { withAdditionalContent = !withAdditionalContent }) } + MaterialButton( + onClick = { imagePositionExpanded = true } + ) { + Text("Image Position: ${imagePosition.name}") + } + DropdownMenu(expanded = imagePositionExpanded, onDismissRequest = { imagePositionExpanded = false }) { + MediaCardImagePosition.entries.forEach { position -> + DropdownMenuItem(onClick = { + imagePosition = position + imagePositionExpanded = false + }) { + Text(position.name) + } + } + } + Spacer(Modifier.height(16.dp)) + MediaCard( modifier = Modifier .padding(24.dp) @@ -103,6 +132,7 @@ fun MediaCards() { primaryButton = if (primaryAction.isNotEmpty()) Action(primaryAction) { } else null, linkButton = if (secondaryAction.isNotEmpty()) Action(secondaryAction) { } else null, + imagePosition = imagePosition, customContent = if (withAdditionalContent) { { Text("Additional content") } } else { diff --git a/doc/images/media_cards/media_card_compose1.png b/doc/images/media_cards/media_card_compose1.png new file mode 100644 index 000000000..ce1513ff7 Binary files /dev/null and b/doc/images/media_cards/media_card_compose1.png differ diff --git a/doc/images/media_cards/media_card_compose2.png b/doc/images/media_cards/media_card_compose2.png new file mode 100644 index 000000000..28dc49cb5 Binary files /dev/null and b/doc/images/media_cards/media_card_compose2.png differ diff --git a/doc/images/media_cards/media_card_compose3.png b/doc/images/media_cards/media_card_compose3.png new file mode 100644 index 000000000..0f84e4e29 Binary files /dev/null and b/doc/images/media_cards/media_card_compose3.png differ diff --git a/doc/images/media_cards/media_card_compose4.png b/doc/images/media_cards/media_card_compose4.png new file mode 100644 index 000000000..8f12b3501 Binary files /dev/null and b/doc/images/media_cards/media_card_compose4.png differ diff --git a/library/screenshots/MediaCard_imageLeft.png b/library/screenshots/MediaCard_imageLeft.png new file mode 100644 index 000000000..d956a0d87 Binary files /dev/null and b/library/screenshots/MediaCard_imageLeft.png differ diff --git a/library/screenshots/MediaCard_imageLeft_dark.png b/library/screenshots/MediaCard_imageLeft_dark.png new file mode 100644 index 000000000..c8ff0c527 Binary files /dev/null and b/library/screenshots/MediaCard_imageLeft_dark.png differ diff --git a/library/screenshots/MediaCard_imageRight.png b/library/screenshots/MediaCard_imageRight.png new file mode 100644 index 000000000..d05aba94b Binary files /dev/null and b/library/screenshots/MediaCard_imageRight.png differ diff --git a/library/screenshots/MediaCard_imageRight_dark.png b/library/screenshots/MediaCard_imageRight_dark.png new file mode 100644 index 000000000..d706afcf9 Binary files /dev/null and b/library/screenshots/MediaCard_imageRight_dark.png differ diff --git a/library/screenshots/MediaCard_imageTop.png b/library/screenshots/MediaCard_imageTop.png new file mode 100644 index 000000000..fe8e7bf39 Binary files /dev/null and b/library/screenshots/MediaCard_imageTop.png differ diff --git a/library/screenshots/MediaCard_imageTop_dark.png b/library/screenshots/MediaCard_imageTop_dark.png new file mode 100644 index 000000000..5664735f9 Binary files /dev/null and b/library/screenshots/MediaCard_imageTop_dark.png differ diff --git a/library/screenshots/MediaCard_withLinkButton_imageLeft.png b/library/screenshots/MediaCard_withLinkButton_imageLeft.png new file mode 100644 index 000000000..1d88b03d1 Binary files /dev/null and b/library/screenshots/MediaCard_withLinkButton_imageLeft.png differ diff --git a/library/screenshots/MediaCard_withLinkButton_imageLeft_dark.png b/library/screenshots/MediaCard_withLinkButton_imageLeft_dark.png new file mode 100644 index 000000000..897b701ca Binary files /dev/null and b/library/screenshots/MediaCard_withLinkButton_imageLeft_dark.png differ diff --git a/library/screenshots/MediaCard_withLinkButton_imageRight.png b/library/screenshots/MediaCard_withLinkButton_imageRight.png new file mode 100644 index 000000000..858dfde31 Binary files /dev/null and b/library/screenshots/MediaCard_withLinkButton_imageRight.png differ diff --git a/library/screenshots/MediaCard_withLinkButton_imageRight_dark.png b/library/screenshots/MediaCard_withLinkButton_imageRight_dark.png new file mode 100644 index 000000000..40e84c926 Binary files /dev/null and b/library/screenshots/MediaCard_withLinkButton_imageRight_dark.png differ diff --git a/library/screenshots/MediaCard_withLinkButton_imageTop.png b/library/screenshots/MediaCard_withLinkButton_imageTop.png new file mode 100644 index 000000000..1b2a8cab6 Binary files /dev/null and b/library/screenshots/MediaCard_withLinkButton_imageTop.png differ diff --git a/library/screenshots/MediaCard_withLinkButton_imageTop_dark.png b/library/screenshots/MediaCard_withLinkButton_imageTop_dark.png new file mode 100644 index 000000000..4ed0ba809 Binary files /dev/null and b/library/screenshots/MediaCard_withLinkButton_imageTop_dark.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_imageLeft.png b/library/screenshots/MediaCard_withPrimaryButton_imageLeft.png new file mode 100644 index 000000000..dc8d15710 Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_imageLeft.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_imageLeft_dark.png b/library/screenshots/MediaCard_withPrimaryButton_imageLeft_dark.png new file mode 100644 index 000000000..e3f1e45ec Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_imageLeft_dark.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_imageRight.png b/library/screenshots/MediaCard_withPrimaryButton_imageRight.png new file mode 100644 index 000000000..e69e9a6d7 Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_imageRight.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_imageRight_dark.png b/library/screenshots/MediaCard_withPrimaryButton_imageRight_dark.png new file mode 100644 index 000000000..b29a31efb Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_imageRight_dark.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_imageTop.png b/library/screenshots/MediaCard_withPrimaryButton_imageTop.png new file mode 100644 index 000000000..28ad2fef5 Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_imageTop.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_imageTop_dark.png b/library/screenshots/MediaCard_withPrimaryButton_imageTop_dark.png new file mode 100644 index 000000000..8fdf077ba Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_imageTop_dark.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageLeft.png b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageLeft.png new file mode 100644 index 000000000..9f2c35238 Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageLeft.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageLeft_dark.png b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageLeft_dark.png new file mode 100644 index 000000000..41c564d9a Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageLeft_dark.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageRight.png b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageRight.png new file mode 100644 index 000000000..349437888 Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageRight.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageRight_dark.png b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageRight_dark.png new file mode 100644 index 000000000..4e7cacf94 Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageRight_dark.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageTop.png b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageTop.png new file mode 100644 index 000000000..8bf63052f Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageTop.png differ diff --git a/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageTop_dark.png b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageTop_dark.png new file mode 100644 index 000000000..fb265cf7c Binary files /dev/null and b/library/screenshots/MediaCard_withPrimaryButton_withLinkButton_imageTop_dark.png differ diff --git a/library/src/main/java/com/telefonica/mistica/card/mediacard/README.md b/library/src/main/java/com/telefonica/mistica/card/mediacard/README.md index 56ff8e2da..cadff9d97 100644 --- a/library/src/main/java/com/telefonica/mistica/card/mediacard/README.md +++ b/library/src/main/java/com/telefonica/mistica/card/mediacard/README.md @@ -38,3 +38,13 @@ fun setOtherMultimedia(view: View) ``` A `VideoView` or any other kind of custom view can be added. + +--- + +## Compose Version Enhancements + +The Jetpack Compose version of MediaCard introduces new features not yet available in the classic View-based version: + +- **Image Position**: The Compose `MediaCard` supports an `imagePosition` parameter, allowing the image to be placed at the top (default), left, or right of the card. The classic version only supports the image at the top. + +For more details, see the [Compose MediaCard README](../../compose/card/mediacard/README.md). diff --git a/library/src/main/java/com/telefonica/mistica/compose/card/Card.kt b/library/src/main/java/com/telefonica/mistica/compose/card/Card.kt index 0dd57ddb9..858a3028e 100644 --- a/library/src/main/java/com/telefonica/mistica/compose/card/Card.kt +++ b/library/src/main/java/com/telefonica/mistica/compose/card/Card.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn @@ -21,11 +20,13 @@ import com.telefonica.mistica.compose.button.Button import com.telefonica.mistica.compose.button.ButtonStyle import com.telefonica.mistica.compose.tag.Tag import com.telefonica.mistica.compose.theme.MisticaTheme +import com.telefonica.mistica.util.applyLinkTextFix @Composable -fun Card( +public fun Card( modifier: Modifier = Modifier, header: @Composable () -> Unit = {}, + invalidatePaddings: Boolean = false, content: @Composable () -> Unit = {}, ) { @@ -41,12 +42,16 @@ fun Card( Column { header() Column( - modifier = Modifier.padding( - start = 16.dp, - top = 16.dp, - end = 16.dp, - bottom = 24.dp, - ), + modifier = if (!invalidatePaddings) + Modifier.padding( + start = 16.dp, + top = 16.dp, + end = 16.dp, + bottom = 24.dp, + ) + else { + Modifier + }, ) { content() } @@ -55,30 +60,32 @@ fun Card( } @Composable -internal fun CardActions(primaryButton: Action?, linkButton: Action?) { +internal fun CardActions( + primaryButton: Action?, + linkButton: Action?, + orientation: CardActionsOrientation = CardActionsOrientation.Horizontal, +) { if (primaryButton != null || linkButton != null) { - Row( - modifier = Modifier - .padding(top = 16.dp) - .width(IntrinsicSize.Max), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - - ) { - primaryButton?.let { - Button( - text = it.text, - onClickListener = it.onTapped, - buttonStyle = ButtonStyle.PRIMARY_SMALL, - ) + when (orientation) { + CardActionsOrientation.Horizontal -> { + Row( + modifier = Modifier + .padding(top = 16.dp) + .width(IntrinsicSize.Max), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + CardActionButtons(primaryButton, linkButton, orientation) + } } - linkButton?.let { - Button( - modifier = if (primaryButton == null) Modifier.offset(x = (-8).dp) else Modifier, - text = it.text, - onClickListener = it.onTapped, - buttonStyle = ButtonStyle.LINK, - ) + CardActionsOrientation.Vertical -> { + Column( + modifier = Modifier.padding(top = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.Start, + ) { + CardActionButtons(primaryButton, linkButton, orientation) + } } } } @@ -135,7 +142,42 @@ internal fun CardContent( } } -data class Action( +@Composable +private fun CardActionButtons( + primaryButton: Action?, + linkButton: Action?, + orientation: CardActionsOrientation, +) { + primaryButton?.let { + Button( + text = it.text, + onClickListener = it.onTapped, + buttonStyle = ButtonStyle.PRIMARY_SMALL, + ) + } + linkButton?.let { + Button( + modifier = if (primaryButton != null && orientation == CardActionsOrientation.Horizontal) { + Modifier.padding(start = 16.dp) + } else { + Modifier + }, + text = it.text.applyLinkTextFix(), + onClickListener = it.onTapped, + buttonStyle = ButtonStyle.LINK, + invalidatePaddings = true, + invalidateMinWidth = true, + ) + } +} + + +public enum class CardActionsOrientation { + Horizontal, + Vertical, +} + +public data class Action( val text: String, val onTapped: () -> Unit, ) diff --git a/library/src/main/java/com/telefonica/mistica/compose/card/highlightedcard/HighLightedCard.kt b/library/src/main/java/com/telefonica/mistica/compose/card/highlightedcard/HighLightedCard.kt index 9de42c078..59db79f4a 100644 --- a/library/src/main/java/com/telefonica/mistica/compose/card/highlightedcard/HighLightedCard.kt +++ b/library/src/main/java/com/telefonica/mistica/compose/card/highlightedcard/HighLightedCard.kt @@ -37,6 +37,7 @@ import com.telefonica.mistica.compose.button.Button import com.telefonica.mistica.compose.button.ButtonStyle import com.telefonica.mistica.compose.theme.MisticaTheme import androidx.compose.ui.platform.LocalResources +import com.telefonica.mistica.util.applyLinkTextFix @Composable fun HighLightedCard( @@ -229,7 +230,7 @@ private fun HighLightCardButton( val isLink = buttonStyle == ButtonStyle.LINK || buttonStyle == ButtonStyle.LINK_INVERSE Button( modifier = modifier, - text = if (isLink) applyLinkTextFix(buttonConfig.buttonText) else buttonConfig.buttonText, + text = if (isLink) buttonConfig.buttonText.applyLinkTextFix() else buttonConfig.buttonText, buttonStyle = buttonStyle, invalidatePaddings = isLink, invalidateMinWidth = isLink, @@ -241,9 +242,6 @@ private fun HighLightCardButton( } } -private fun applyLinkTextFix(text: String): String { - return text.padEnd(18, ' ') -} enum class HighLightCardImageConfig { FIT, diff --git a/library/src/main/java/com/telefonica/mistica/compose/card/mediacard/MediaCard.kt b/library/src/main/java/com/telefonica/mistica/compose/card/mediacard/MediaCard.kt index f4bf08ea0..cb15c36c5 100644 --- a/library/src/main/java/com/telefonica/mistica/compose/card/mediacard/MediaCard.kt +++ b/library/src/main/java/com/telefonica/mistica/compose/card/mediacard/MediaCard.kt @@ -2,16 +2,26 @@ package com.telefonica.mistica.compose.card.mediacard import androidx.annotation.DrawableRes import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.telefonica.mistica.R import com.telefonica.mistica.compose.card.Action import com.telefonica.mistica.compose.card.Card import com.telefonica.mistica.compose.card.CardActions +import com.telefonica.mistica.compose.card.CardActionsOrientation import com.telefonica.mistica.compose.card.CardContent import com.telefonica.mistica.compose.card.mediacard.MediaCardImage.MediaCardImageBitmap import com.telefonica.mistica.compose.card.mediacard.MediaCardImage.MediaCardImageResource @@ -19,8 +29,10 @@ import com.telefonica.mistica.compose.tag.Tag import com.telefonica.mistica.tag.TagView import com.telefonica.mistica.util.PreviewTheme +private val SIDE_IMAGE_WIDTH = 150.dp + @Composable -fun MediaCard( +public fun MediaCard( image: MediaCardImage, modifier: Modifier = Modifier, tag: Tag? = null, @@ -30,46 +42,124 @@ fun MediaCard( description: String? = null, primaryButton: Action? = null, linkButton: Action? = null, + imagePosition: MediaCardImagePosition = MediaCardImagePosition.Top, + imageContentScale: ContentScale = ContentScale.Crop, customContent: @Composable () -> Unit = {}, ) { - Card( - modifier = modifier, - header = { CardImage(image) } - ) { - CardContent(tag, preTitle, title, subtitle, description) - customContent() - CardActions(primaryButton, linkButton) + when (imagePosition) { + MediaCardImagePosition.Top -> { + Card( + modifier = modifier, + header = { + CardImage( + mediaCardImage = image, + modifier = Modifier.fillMaxWidth(), + contentScale = imageContentScale, + ) + } + ) { + CardContent(tag, preTitle, title, subtitle, description) + customContent() + CardActions(primaryButton, linkButton) + } + } + MediaCardImagePosition.Left -> { + Card( + modifier = modifier, + invalidatePaddings = true, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + ) { + CardImage( + mediaCardImage = image, + modifier = Modifier + .width(SIDE_IMAGE_WIDTH) + .fillMaxHeight(), + contentScale = imageContentScale, + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 24.dp) + ) { + CardContent(tag, preTitle, title, subtitle, description) + customContent() + CardActions(primaryButton, linkButton, orientation = CardActionsOrientation.Vertical) + } + } + } + } + MediaCardImagePosition.Right -> { + Card( + modifier = modifier, + invalidatePaddings = true, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 24.dp) + ) { + CardContent(tag, preTitle, title, subtitle, description) + customContent() + CardActions(primaryButton, linkButton, orientation = CardActionsOrientation.Vertical) + } + CardImage( + mediaCardImage = image, + modifier = Modifier + .width(SIDE_IMAGE_WIDTH) + .fillMaxHeight(), + contentScale = imageContentScale, + ) + } + } + } } } -sealed class MediaCardImage(val contentDescription: String?) { - class MediaCardImageResource(@DrawableRes val imageRes: Int, contentDescription: String? = null) : MediaCardImage(contentDescription) - class MediaCardImageBitmap(val imageBitmap: ImageBitmap, contentDescription: String? = null) : MediaCardImage(contentDescription) +public sealed class MediaCardImage(public val contentDescription: String?) { + public class MediaCardImageResource(public @DrawableRes val imageRes: Int, contentDescription: String? = null) : MediaCardImage(contentDescription) + public class MediaCardImageBitmap(public val imageBitmap: ImageBitmap, contentDescription: String? = null) : MediaCardImage(contentDescription) +} + +public enum class MediaCardImagePosition { + Top, + Left, + Right } @Composable -private fun CardImage(mediaCardImage: MediaCardImage) { +private fun CardImage(mediaCardImage: MediaCardImage, modifier: Modifier = Modifier, contentScale: ContentScale) { when (mediaCardImage) { is MediaCardImageBitmap -> Image( mediaCardImage.imageBitmap, contentDescription = mediaCardImage.contentDescription, - contentScale = ContentScale.FillHeight + contentScale = contentScale, + modifier = modifier, ) is MediaCardImageResource -> Image( painterResource(id = mediaCardImage.imageRes), contentDescription = mediaCardImage.contentDescription, - contentScale = ContentScale.FillHeight + contentScale = contentScale, + modifier = modifier, ) } } @Preview @Composable -fun CardPreview() { +private fun PreviewMediaCardImageTop() { PreviewTheme { MediaCard( - image = MediaCardImageResource(R.drawable.bg_list_image), + image = MediaCardImageResource(R.drawable.mistica_placeholder), tag = Tag("HEADLINE").withStyle(TagView.TYPE_PROMO), preTitle = "Pretitle", title = "Title", @@ -79,4 +169,40 @@ fun CardPreview() { linkButton = Action("Link") {} ) } +} + +@Preview +@Composable +private fun PreviewMediaCardImageLeft() { + PreviewTheme { + MediaCard( + image = MediaCardImageResource(R.drawable.mistica_placeholder), + tag = Tag("HEADLINE").withStyle(TagView.TYPE_PROMO), + preTitle = "Pretitle", + title = "Title", + subtitle = "Subtitle", + description = "Description", + primaryButton = Action("Primary") {}, + linkButton = Action("Link") {}, + imagePosition = MediaCardImagePosition.Left, + ) + } +} + +@Preview +@Composable +private fun PreviewMediaCardImageRight() { + PreviewTheme { + MediaCard( + image = MediaCardImageResource(R.drawable.mistica_placeholder), + tag = Tag("HEADLINE").withStyle(TagView.TYPE_PROMO), + preTitle = "Pretitle", + title = "Title", + subtitle = "Subtitle", + description = "Description", + primaryButton = Action("Primary") {}, + linkButton = Action("Link") {}, + imagePosition = MediaCardImagePosition.Right, + ) + } } \ No newline at end of file diff --git a/library/src/main/java/com/telefonica/mistica/compose/card/mediacard/README.md b/library/src/main/java/com/telefonica/mistica/compose/card/mediacard/README.md index 79c7f02a3..49bfee962 100644 --- a/library/src/main/java/com/telefonica/mistica/compose/card/mediacard/README.md +++ b/library/src/main/java/com/telefonica/mistica/compose/card/mediacard/README.md @@ -1,10 +1,121 @@ # Media Card -Media cards consist of an image and some data: +Media cards combine an image with textual content and actions, providing a rich visual component for displaying content. -

- - -

+## Examples -To use it, call [`MediaCard()`](https://github.com/Telefonica/mistica-android/blob/main/library/src/main/java/com/telefonica/mistica/compose/card/mediacard/MediaCard.kt#L24) +| Description | Preview | +|-----------------------------------------|:----------------------------------------------------------------------------------------------------:| +| Image Position Top | ![MediaCard Top Light](../../../../../../../../../../doc/images/media_cards/media_card_compose1.png) | +| Image Position Top with tag and buttons | ![MediaCard Top Dark](../../../../../../../../../../doc/images/media_cards/media_card_compose2.png) | +| Image Position Left | ![MediaCard Left](../../../../../../../../../../doc/images/media_cards/media_card_compose3.png) | +| Image Position Right | ![MediaCard Right](../../../../../../../../../../doc/images/media_cards/media_card_compose4.png) | + +## Usage + +To create a Media Card, use the [`MediaCard()`](https://github.com/Telefonica/mistica-android/blob/main/library/src/main/java/com/telefonica/mistica/compose/card/mediacard/MediaCard.kt#L24) composable: + +```kotlin +MediaCard( + image = MediaCardImageResource(R.drawable.my_image), + tag = Tag("PROMO").withStyle(TagView.TYPE_PROMO), + preTitle = "Pre-title text", + title = "Card Title", + subtitle = "Card subtitle", + description = "A detailed description of the content", + primaryButton = Action("Primary") { /* action */ }, + linkButton = Action("Link") { /* action */ } +) +``` + +## Image Configuration + +### Image Types + +Media Card supports two image types via the sealed class `MediaCardImage`: + +- **`MediaCardImageResource`**: Display an image from drawable resources + ```kotlin + MediaCardImageResource( + imageRes = R.drawable.my_image, + contentDescription = "Description for accessibility" + ) + ``` + +- **`MediaCardImageBitmap`**: Display an `ImageBitmap` directly + ```kotlin + MediaCardImageBitmap( + imageBitmap = myImageBitmap, + contentDescription = "Description for accessibility" + ) + ``` + +### Image Position + +Control where the image appears using the `imagePosition` parameter: + +- **`MediaCardImagePosition.Top`** (default): Image spans the full width at the top +- **`MediaCardImagePosition.Left`**: Image on the left side (150dp width) +- **`MediaCardImagePosition.Right`**: Image on the right side (150dp width) + +```kotlin +MediaCard( + image = MediaCardImageResource(R.drawable.my_image), + imagePosition = MediaCardImagePosition.Left, + // ... other parameters +) +``` + +### Image Content Scale + +Customize how the image scales using the `imageContentScale` parameter (default: `ContentScale.Crop`): + +```kotlin +MediaCard( + image = MediaCardImageResource(R.drawable.my_image), + imageContentScale = ContentScale.Fit, + // ... other parameters +) +``` + +## Content Elements + +All content elements are optional: + +- **`tag`**: A `Tag` displayed at the top (e.g., "NEW", "PROMO") +- **`preTitle`**: Text displayed above the title +- **`title`**: The main heading +- **`subtitle`**: Secondary text below the title +- **`description`**: Detailed description text + +## Actions + +Add up to two action buttons: + +- **`primaryButton`**: Primary call-to-action button +- **`linkButton`**: Secondary link-style button + +When the image is positioned at `Left` or `Right`, buttons stack vertically. With `Top` image position, buttons are arranged horizontally. + +```kotlin +MediaCard( + image = MediaCardImageResource(R.drawable.my_image), + primaryButton = Action("Buy Now") { /* handle action */ }, + linkButton = Action("Learn More") { /* handle action */ } +) +``` + +## Custom Content + +Inject custom composable content between the description and actions using the `customContent` parameter: + +```kotlin +MediaCard( + image = MediaCardImageResource(R.drawable.my_image), + title = "Card with Custom Content", + customContent = { + // Your custom composable content here + Text("Custom content") + } +) +``` diff --git a/library/src/main/java/com/telefonica/mistica/util/TextUtils.kt b/library/src/main/java/com/telefonica/mistica/util/TextUtils.kt index 5db787103..854b9cdde 100644 --- a/library/src/main/java/com/telefonica/mistica/util/TextUtils.kt +++ b/library/src/main/java/com/telefonica/mistica/util/TextUtils.kt @@ -4,6 +4,8 @@ import android.view.View import android.widget.TextView import androidx.annotation.StyleRes +private const val MIN_LINK_TEXT_LENGTH = 18 + fun TextView.setTextAndVisibility(newText: CharSequence?) { if (newText?.isNotBlank() == true) { text = newText @@ -15,4 +17,12 @@ fun TextView.setTextAndVisibility(newText: CharSequence?) { fun TextView.setTextPreset(@StyleRes textPreset: Int) { setTextAppearance(textPreset) +} + +/** + * Applies padding to a link text to ensure proper alignment at the start + * even when the text consists of few characters. + */ +internal fun String.applyLinkTextFix(): String { + return this.padEnd(MIN_LINK_TEXT_LENGTH, ' ') } \ No newline at end of file diff --git a/library/src/main/res/drawable-nodpi/mistica_placeholder.png b/library/src/main/res/drawable-nodpi/mistica_placeholder.png new file mode 100644 index 000000000..bc88219ec Binary files /dev/null and b/library/src/main/res/drawable-nodpi/mistica_placeholder.png differ diff --git a/library/src/test/java/com/telefonica/mistica/compose/card/mediacard/MediaCardTest.kt b/library/src/test/java/com/telefonica/mistica/compose/card/mediacard/MediaCardTest.kt new file mode 100644 index 000000000..3ab2b8b6c --- /dev/null +++ b/library/src/test/java/com/telefonica/mistica/compose/card/mediacard/MediaCardTest.kt @@ -0,0 +1,113 @@ +package com.telefonica.mistica.compose.card.mediacard + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.unit.dp +import com.telefonica.mistica.R +import com.telefonica.mistica.compose.card.Action +import com.telefonica.mistica.compose.card.mediacard.MediaCardImage.MediaCardImageResource +import com.telefonica.mistica.compose.tag.Tag +import com.telefonica.mistica.compose.theme.MisticaTheme +import com.telefonica.mistica.compose.theme.brand.MovistarBrand +import com.telefonica.mistica.tag.TagView +import com.telefonica.mistica.testutils.ScreenshotsTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner + +@RunWith(ParameterizedRobolectricTestRunner::class) +internal class MediaCardTest( + private val darkTheme: Boolean, + private val primaryButtonText: String, + private val linkButtonText: String, + private val imagePosition: MediaCardImagePosition +) : ScreenshotsTest() { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun `check mediacard screenshot`() { + `when mediaCard`( + darkTheme = darkTheme, + primaryButtonText = primaryButtonText, + linkButtonText = linkButtonText, + imagePosition = imagePosition, + ) + `then screenshot is OK`(darkTheme, primaryButtonText, linkButtonText, imagePosition) + } + + private fun `when mediaCard`( + darkTheme: Boolean = false, + primaryButtonText: String = "", + linkButtonText: String = "", + imagePosition: MediaCardImagePosition = MediaCardImagePosition.Top, + customContent: @Composable () -> Unit = {}, + ) { + composeTestRule.setContent { + MisticaTheme(brand = MovistarBrand, darkTheme = darkTheme) { + MediaCard( + modifier = Modifier + .padding(24.dp) + .fillMaxWidth(), + image = MediaCardImageResource(R.drawable.mistica_placeholder), + tag = Tag("Tag").withStyle(TagView.TYPE_PROMO), + preTitle = "pre title", + title = "Title", + subtitle = "Media Card subtitle", + description = "Media Card description", + primaryButton = if (primaryButtonText.isNotEmpty()) Action(primaryButtonText) { } else null, + linkButton = if (linkButtonText.isNotEmpty()) Action(linkButtonText) { } else null, + imagePosition = imagePosition, + customContent = customContent, + ) + } + } + } + + private fun `then screenshot is OK`( + darkTheme: Boolean = false, + primaryButtonText: String = "", + linkButtonText: String = "", + imagePosition: MediaCardImagePosition = MediaCardImagePosition.Top, + ) { + val extra: String? = mutableListOf().apply { + primaryButtonText.takeIf { it.isNotEmpty() }?.let { add("withPrimaryButton") } + linkButtonText.takeIf { it.isNotEmpty() }?.let { add("withLinkButton") } + add("image${imagePosition.name}") + }.takeIf { + it.isNotEmpty() + }?.joinToString(separator = "_") + + compareScreenshot( + node = composeTestRule.onRoot(), + component = "MediaCard", + darkTheme = darkTheme, + extra = extra + ) + } + + companion object { + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters(name = "MediaCard darkTheme={0} primary={1} link={2} position={3}") + fun params(): List> { + val darkThemes = listOf(false, true) + val primaryButtons = listOf("", "Primary") + val linkButtons = listOf("", "Link") + val positions = MediaCardImagePosition.entries + return darkThemes.flatMap { darkTheme -> + primaryButtons.flatMap { primary -> + linkButtons.flatMap { link -> + positions.map { pos -> + arrayOf(darkTheme, primary, link, pos) + } + } + } + } + } + } +} \ No newline at end of file