From 2f43ca42b4dd7e021003109d12cd5fa736c5c379 Mon Sep 17 00:00:00 2001 From: Eduarda Barbosa <42220351+mebarbosa@users.noreply.github.com> Date: Fri, 31 Jan 2025 08:54:14 -0300 Subject: [PATCH] Migrate Podcast Bottom Header to Jetpack Compose (#3513) --- .../view/components/ratings/StarRatingView.kt | 37 ++--- .../podcasts/view/podcast/PodcastAdapter.kt | 98 ++++++------ .../view/podcast/PodcastHeaderBottom.kt | 120 ++++++++++++++ .../res/layout/view_podcast_header_bottom.xml | 100 +----------- .../compose/components/TextStyles.kt | 6 +- .../pocketcasts/compose/text/Extension.kt | 149 ++++++++++++++++++ 6 files changed, 338 insertions(+), 172 deletions(-) create mode 100644 modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastHeaderBottom.kt create mode 100644 modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/text/Extension.kt diff --git a/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/components/ratings/StarRatingView.kt b/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/components/ratings/StarRatingView.kt index 28f73575d41..196e1be4575 100644 --- a/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/components/ratings/StarRatingView.kt +++ b/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/components/ratings/StarRatingView.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding -import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable @@ -18,13 +17,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.fragment.app.FragmentManager import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground -import au.com.shiftyjelly.pocketcasts.compose.buttons.RowOutlinedButton +import au.com.shiftyjelly.pocketcasts.compose.components.TextH30 import au.com.shiftyjelly.pocketcasts.compose.components.TextP40 import au.com.shiftyjelly.pocketcasts.compose.preview.ThemePreviewParameterProvider import au.com.shiftyjelly.pocketcasts.compose.theme @@ -43,6 +43,7 @@ import au.com.shiftyjelly.pocketcasts.localization.R as LR fun StarRatingView( fragmentManager: FragmentManager, viewModel: PodcastRatingsViewModel, + modifier: Modifier = Modifier, ) { val state by viewModel.stateFlow.collectAsState() @@ -51,6 +52,7 @@ fun StarRatingView( val loadedState = state as RatingState.Loaded Content( state = loadedState, + modifier = modifier, onClick = { source -> viewModel.onRatingStarsTapped( podcastUuid = loadedState.podcastUuid, @@ -70,26 +72,23 @@ fun StarRatingView( @Composable private fun Content( state: RatingState.Loaded, + modifier: Modifier = Modifier, onClick: (RatingTappedSource) -> Unit, ) { val starsContentDescription = stringResource(LR.string.podcast_star_rating_content_description) Row( - modifier = Modifier.padding( - start = 8.dp, - end = 4.dp, - top = 8.dp, - ), + modifier = modifier.padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .clickable { onClick(RatingTappedSource.STARS) } - .padding(8.dp) .semantics { this.contentDescription = starsContentDescription - }, + } + .padding(8.dp), ) { Stars( stars = state.stars, @@ -112,20 +111,16 @@ private fun Content( Spacer(modifier = Modifier.weight(1f)) - RowOutlinedButton( + TextH30( text = stringResource(R.string.rate_button), - onClick = { onClick(RatingTappedSource.BUTTON) }, - includePadding = false, - fontSize = 16.sp, - textPadding = 0.dp, fontWeight = FontWeight.W500, - border = null, - fullWidth = false, - colors = ButtonDefaults.outlinedButtonColors( - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.theme.colors.primaryText01, - ), - modifier = Modifier.padding(top = 2.dp, bottom = 2.dp), + textAlign = TextAlign.Center, + fontSize = 16.sp, + modifier = Modifier + .clickable { + onClick(RatingTappedSource.BUTTON) + } + .padding(8.dp), ) } } diff --git a/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastAdapter.kt b/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastAdapter.kt index 40e62453164..e762e9a19c0 100644 --- a/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastAdapter.kt +++ b/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastAdapter.kt @@ -9,7 +9,6 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build -import android.text.Html import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan @@ -18,11 +17,14 @@ import android.view.View import android.view.ViewGroup import android.view.animation.AccelerateDecelerateInterpolator import android.widget.TextView +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat -import androidx.core.content.ContextCompat.startActivity +import androidx.core.text.HtmlCompat import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import androidx.interpolator.view.animation.FastOutSlowInInterpolator @@ -32,6 +34,7 @@ import androidx.transition.ChangeBounds import androidx.transition.TransitionManager import au.com.shiftyjelly.pocketcasts.analytics.SourceView import au.com.shiftyjelly.pocketcasts.compose.AppTheme +import au.com.shiftyjelly.pocketcasts.compose.text.toAnnotatedString import au.com.shiftyjelly.pocketcasts.models.entity.BaseEpisode import au.com.shiftyjelly.pocketcasts.models.entity.Bookmark import au.com.shiftyjelly.pocketcasts.models.entity.Podcast @@ -43,7 +46,6 @@ import au.com.shiftyjelly.pocketcasts.podcasts.R import au.com.shiftyjelly.pocketcasts.podcasts.databinding.AdapterEpisodeBinding import au.com.shiftyjelly.pocketcasts.podcasts.databinding.AdapterEpisodeHeaderBinding import au.com.shiftyjelly.pocketcasts.podcasts.databinding.AdapterPodcastHeaderBinding -import au.com.shiftyjelly.pocketcasts.podcasts.helper.readMore import au.com.shiftyjelly.pocketcasts.podcasts.view.components.PlayButton import au.com.shiftyjelly.pocketcasts.podcasts.view.components.ratings.StarRatingView import au.com.shiftyjelly.pocketcasts.podcasts.view.podcast.adapter.BookmarkHeaderViewHolder @@ -54,7 +56,6 @@ import au.com.shiftyjelly.pocketcasts.podcasts.view.podcast.adapter.TabsViewHold import au.com.shiftyjelly.pocketcasts.podcasts.viewmodel.PodcastRatingsViewModel import au.com.shiftyjelly.pocketcasts.podcasts.viewmodel.PodcastViewModel.PodcastTab import au.com.shiftyjelly.pocketcasts.preferences.Settings -import au.com.shiftyjelly.pocketcasts.preferences.model.ArtworkConfiguration import au.com.shiftyjelly.pocketcasts.preferences.model.ArtworkConfiguration.Element import au.com.shiftyjelly.pocketcasts.repositories.download.DownloadManager import au.com.shiftyjelly.pocketcasts.repositories.images.PocketCastsImageRequestFactory @@ -70,7 +71,6 @@ import au.com.shiftyjelly.pocketcasts.utils.featureflag.FeatureFlag import au.com.shiftyjelly.pocketcasts.views.extensions.hide import au.com.shiftyjelly.pocketcasts.views.extensions.show import au.com.shiftyjelly.pocketcasts.views.extensions.toggleVisibility -import au.com.shiftyjelly.pocketcasts.views.extensions.trimPadding import au.com.shiftyjelly.pocketcasts.views.helper.AnimatorUtil import au.com.shiftyjelly.pocketcasts.views.helper.SwipeButtonLayoutFactory import au.com.shiftyjelly.pocketcasts.views.helper.toCircle @@ -232,7 +232,7 @@ class PodcastAdapter( VIEW_TYPE_NO_BOOKMARK -> NoBookmarkViewHolder(ComposeView(parent.context), theme, onHeadsetSettingsClicked) else -> EpisodeViewHolder( binding = AdapterEpisodeBinding.inflate(inflater, parent, false), - viewMode = if (settings.artworkConfiguration.value.useEpisodeArtwork(ArtworkConfiguration.Element.Podcasts)) { + viewMode = if (settings.artworkConfiguration.value.useEpisodeArtwork(Element.Podcasts)) { EpisodeViewHolder.ViewMode.Artwork } else { EpisodeViewHolder.ViewMode.NoArtwork @@ -243,7 +243,7 @@ class PodcastAdapter( imageRequestFactory = imageRequestFactory.smallSize(), settings = settings, swipeButtonLayoutFactory = swipeButtonLayoutFactory, - artworkContext = ArtworkConfiguration.Element.Podcasts, + artworkContext = Element.Podcasts, ) } } @@ -273,28 +273,6 @@ class PodcastAdapter( bindHeaderBottom(holder) bindHeaderTop(holder) - holder.binding.bottom.ratings.setContent { - AppTheme(theme.activeTheme) { - StarRatingView(fragmentManager, ratingsViewModel) - } - } - - holder.binding.bottom.podcastInfoPanel.setContent { - AppTheme(theme.activeTheme) { - PodcastInfoView( - PodcastInfoState( - author = podcast.author, - link = podcast.getShortUrl(), - schedule = podcast.displayableFrequency(context.resources), - next = podcast.displayableNextEpisodeDate(context), - ), - onWebsiteLinkClicked = { - onWebsiteLinkClicked(context) - }, - ) - } - } - val imageView = holder.binding.top.artwork // stopping the artwork flickering when the image is reloaded if (imageView.drawable == null || holder.lastImagePodcastUuid == null || holder.lastImagePodcastUuid != podcast.uuid) { @@ -314,26 +292,45 @@ class PodcastAdapter( private fun bindHeaderBottom(holder: PodcastViewHolder) { holder.binding.bottom.root.isVisible = headerExpanded - val tintColor = ThemeColor.podcastText02(theme.activeTheme, tintColor) - holder.binding.bottom.title.text = podcast.title - holder.binding.bottom.title.readMore(3) - with(holder.binding.bottom.category) { - text = podcast.getFirstCategory(context.resources) - } - holder.binding.bottom.description.text = - if (FeatureFlag.isEnabled(Feature.PODCAST_HTML_DESCRIPTION) && podcast.podcastHtmlDescription.isNotEmpty()) { - // keep the extra line break from paragraphs as it looks better - Html.fromHtml( - podcast.podcastHtmlDescription, - Html.FROM_HTML_MODE_COMPACT and - Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH.inv(), - ).trimPadding() - } else { - podcast.podcastDescription + + val isHtmlDescription = FeatureFlag.isEnabled(Feature.PODCAST_HTML_DESCRIPTION) && podcast.podcastHtmlDescription.isNotEmpty() + val descriptionLinkColor: Int = ThemeColor.podcastText02(theme.activeTheme, tintColor) + + val description = if (isHtmlDescription) { + HtmlCompat.fromHtml( + podcast.podcastHtmlDescription, + HtmlCompat.FROM_HTML_MODE_COMPACT and + HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH.inv(), // keep the extra line break from paragraphs as it looks better + ).toAnnotatedString(urlColor = descriptionLinkColor) + } else { + AnnotatedString(podcast.podcastDescription) + } + + holder.binding.bottom.podcastHeaderBottom.setContent { + AppTheme(theme.activeTheme) { + PodcastHeaderBottom( + title = podcast.title, + category = podcast.getFirstCategory(context.resources), + description = description, + podcastInfoContent = { + PodcastInfoView( + PodcastInfoState( + author = podcast.author, + link = podcast.getShortUrl(), + schedule = podcast.displayableFrequency(context.resources), + next = podcast.displayableNextEpisodeDate(context), + ), + onWebsiteLinkClicked = { + onWebsiteLinkClicked(context) + }, + ) + }, + ratingsContent = { + StarRatingView(fragmentManager, ratingsViewModel, Modifier.padding(top = 8.dp)) + }, + onDescriptionClicked = onPodcastDescriptionClicked, + ) } - holder.binding.bottom.description.setLinkTextColor(tintColor) - holder.binding.bottom.description.readMore(3) { - onPodcastDescriptionClicked() } } @@ -750,9 +747,6 @@ class PodcastAdapter( binding.top.settings.setOnClickListener { adapter.onSettingsClicked() } - binding.bottom.ratings.setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, - ) } private fun unsubscribe() { diff --git a/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastHeaderBottom.kt b/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastHeaderBottom.kt new file mode 100644 index 00000000000..c5b535c6e56 --- /dev/null +++ b/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastHeaderBottom.kt @@ -0,0 +1,120 @@ +package au.com.shiftyjelly.pocketcasts.podcasts.view.podcast + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import au.com.shiftyjelly.pocketcasts.compose.components.TextH20 +import au.com.shiftyjelly.pocketcasts.compose.components.TextP40 +import au.com.shiftyjelly.pocketcasts.compose.theme + +private const val collapsedLines: Int = 3 + +@Composable +fun PodcastHeaderBottom( + title: String, + category: String, + description: AnnotatedString, + onDescriptionClicked: () -> Unit, + modifier: Modifier = Modifier, + ratingsContent: @Composable () -> Unit = {}, + podcastInfoContent: @Composable () -> Unit = {}, +) { + var isTitleExpanded by remember { mutableStateOf(false) } + var isDescriptionExpanded by remember { mutableStateOf(false) } + val interactionSource = remember { MutableInteractionSource() } + + Column( + modifier = modifier + .fillMaxWidth() + .clipToBounds() + .padding(bottom = 8.dp) + .padding(horizontal = 16.dp) + .background(MaterialTheme.theme.colors.primaryUi02), + ) { + TextH20( + text = title, + maxLines = if (isTitleExpanded) Int.MAX_VALUE else collapsedLines, + overflow = if (isTitleExpanded) TextOverflow.Clip else TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp) + .animateContentSize( + animationSpec = tween( + durationMillis = 200, + easing = FastOutSlowInEasing, + ), + ) + .clickable( + indication = null, + interactionSource = interactionSource, + onClick = { + isTitleExpanded = !isTitleExpanded + }, + ), + ) + + TextP40( + text = category, + fontSize = 15.sp, + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp), + color = MaterialTheme.theme.colors.primaryText02, + ) + + ratingsContent() + + Divider( + color = MaterialTheme.theme.colors.primaryUi05, + thickness = 1.dp, + ) + + Text( + text = description, + maxLines = if (isDescriptionExpanded) Int.MAX_VALUE else collapsedLines, + overflow = if (isDescriptionExpanded) TextOverflow.Clip else TextOverflow.Ellipsis, + lineHeight = 21.sp, + fontSize = 16.sp, + color = MaterialTheme.theme.colors.primaryText01, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 16.dp) + .animateContentSize( + animationSpec = tween( + durationMillis = 200, + easing = FastOutSlowInEasing, + ), + ) + .clickable( + indication = null, + interactionSource = interactionSource, + onClick = { + isDescriptionExpanded = !isDescriptionExpanded + onDescriptionClicked.invoke() + }, + ), + ) + + podcastInfoContent() + } +} diff --git a/modules/features/podcasts/src/main/res/layout/view_podcast_header_bottom.xml b/modules/features/podcasts/src/main/res/layout/view_podcast_header_bottom.xml index b4b5fa04042..9b3a15cce05 100644 --- a/modules/features/podcasts/src/main/res/layout/view_podcast_header_bottom.xml +++ b/modules/features/podcasts/src/main/res/layout/view_podcast_header_bottom.xml @@ -1,100 +1,6 @@ - - - - - - - - - - - - - - - - - + android:visibility="gone" /> \ No newline at end of file diff --git a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/TextStyles.kt b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/TextStyles.kt index ef1e5005b1d..80cf565a8e4 100644 --- a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/TextStyles.kt +++ b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/TextStyles.kt @@ -60,6 +60,7 @@ fun TextH20( color: Color = MaterialTheme.theme.colors.primaryText01, maxLines: Int = Int.MAX_VALUE, textAlign: TextAlign? = null, + overflow: TextOverflow = TextOverflow.Ellipsis, disableAutoScale: Boolean = false, fontSize: TextUnit = 22.sp, lineHeight: TextUnit = 30.sp, @@ -72,7 +73,7 @@ fun TextH20( lineHeight = lineHeight.scaled(disableAutoScale, fontScale), fontWeight = FontWeight.W700, maxLines = maxLines, - overflow = TextOverflow.Ellipsis, + overflow = overflow, textAlign = textAlign, modifier = modifier, ) @@ -166,6 +167,7 @@ fun TextP40( color: Color = MaterialTheme.theme.colors.primaryText01, maxLines: Int = Int.MAX_VALUE, disableAutoScale: Boolean = false, + overflow: TextOverflow = TextOverflow.Ellipsis, fontFamily: FontFamily? = null, fontWeight: FontWeight? = null, fontSize: TextUnit = 16.sp, @@ -179,7 +181,7 @@ fun TextP40( lineHeight = lineHeight.scaled(disableAutoScale, fontScale), textAlign = textAlign, maxLines = maxLines, - overflow = TextOverflow.Ellipsis, + overflow = overflow, fontFamily = fontFamily, fontWeight = fontWeight, modifier = modifier, diff --git a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/text/Extension.kt b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/text/Extension.kt new file mode 100644 index 00000000000..ae644c24f90 --- /dev/null +++ b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/text/Extension.kt @@ -0,0 +1,149 @@ +package au.com.shiftyjelly.pocketcasts.compose.text + +import android.graphics.Typeface +import android.text.Spanned +import android.text.style.BackgroundColorSpan +import android.text.style.ForegroundColorSpan +import android.text.style.RelativeSizeSpan +import android.text.style.StrikethroughSpan +import android.text.style.StyleSpan +import android.text.style.SubscriptSpan +import android.text.style.SuperscriptSpan +import android.text.style.URLSpan +import android.text.style.UnderlineSpan +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.BaselineShift +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.sp + +// See: https://iamjosephmj.medium.com/how-to-display-styled-strings-in-jetpack-compose-decd6b705746 + +fun Spanned.toAnnotatedString(urlColor: Int? = null): AnnotatedString = buildAnnotatedString { + // Step 1: Copy over the raw text + append(this@toAnnotatedString.toString()) + // Step 2: Go through each span + getSpans(0, length, Any::class.java).forEach { span -> + val start = getSpanStart(span) + val end = getSpanEnd(span) + when (span) { + // Bold, Italic, Bold-Italic + is StyleSpan -> { + when (span.style) { + Typeface.BOLD -> addStyle( + SpanStyle(fontWeight = FontWeight.Bold), + start, + end, + ) + + Typeface.ITALIC -> addStyle( + SpanStyle(fontStyle = FontStyle.Italic), + start, + end, + ) + + Typeface.BOLD_ITALIC -> addStyle( + SpanStyle( + fontWeight = FontWeight.Bold, + fontStyle = FontStyle.Italic, + ), + start, + end, + ) + } + } + // Underline + is UnderlineSpan -> { + addStyle( + SpanStyle(textDecoration = TextDecoration.Underline), + start, + end, + ) + } + // Foreground Color + is ForegroundColorSpan -> { + addStyle( + SpanStyle(color = Color(span.foregroundColor)), + start, + end, + ) + } + // Background Color + is BackgroundColorSpan -> { + addStyle( + SpanStyle(background = Color(span.backgroundColor)), + start, + end, + ) + } + // Strikethrough (Line-through) + is StrikethroughSpan -> { + addStyle( + SpanStyle(textDecoration = TextDecoration.LineThrough), + start, + end, + ) + } + // Relative Size (scales the text) + is RelativeSizeSpan -> { + // For a real-world app, you'd need the base font size to multiply by span.sizeChange. + // Here, for simplicity, let's assume a base size or do a rough conversion: + val baseFontSize = 16.sp + val newFontSize = baseFontSize * span.sizeChange + addStyle( + SpanStyle(fontSize = newFontSize), + start, + end, + ) + } + // URL or clickable text + is URLSpan -> { + // You can store the URL as an annotation and optionally add a style + addStringAnnotation( + tag = "URL", + annotation = span.url, + start = start, + end = end, + ) + // Optional: add a style (color or underline) to make it look clickable + val color = if (urlColor != null) Color(urlColor) else Color.Blue + addStyle( + SpanStyle( + color = color, + textDecoration = TextDecoration.Underline, + ), + start, + end, + ) + } + // Subscript + is SubscriptSpan -> { + // Compose doesn't have a built-in subscript style, + // so you'd either skip or handle it with a custom solution + // For demonstration, let's apply a smaller font size + val baseFontSize = 16.sp + addStyle( + SpanStyle(fontSize = baseFontSize * 0.8f, baselineShift = BaselineShift.Subscript), + start, + end, + ) + } + // Superscript + is SuperscriptSpan -> { + // Similarly, let's demonstrate a smaller font size with a shift + val baseFontSize = 16.sp + addStyle( + SpanStyle(fontSize = baseFontSize * 0.8f, baselineShift = BaselineShift.Superscript), + start, + end, + ) + } + // You can keep adding more span types as needed + else -> {} + } + } +}