@@ -28,7 +28,6 @@ import androidx.compose.ui.graphics.StrokeCap
2828import androidx.compose.ui.graphics.isSpecified
2929import androidx.compose.ui.layout.layout
3030import androidx.compose.ui.text.AnnotatedString
31- import androidx.compose.ui.text.LinkAnnotation
3231import androidx.compose.ui.text.SpanStyle
3332import androidx.compose.ui.text.TextLayoutResult
3433import androidx.compose.ui.text.buildAnnotatedString
@@ -116,7 +115,7 @@ public fun RichTextScope.Text(
116115 val inlineContents = decoratedTextResult.inlineContents
117116 val decoratedLinkRanges = decoratedTextResult.decoratedLinkRanges
118117 var textLayoutResult by remember { mutableStateOf<TextLayoutResult ?>(null ) }
119- val animatedText = if (renderOptions.animate && inlineContents.isEmpty()) {
118+ val animatedTextResult = if (renderOptions.animate && inlineContents.isEmpty()) {
120119 rememberAnimatedText(
121120 annotated = decoratedTextResult.annotatedString,
122121 contentColor = contentColor,
@@ -125,44 +124,43 @@ public fun RichTextScope.Text(
125124 sharedAnimationState = sharedAnimationState,
126125 )
127126 } else {
128- decoratedTextResult.annotatedString
127+ remember(decoratedTextResult.annotatedString) {
128+ AnimatedTextResult (
129+ text = decoratedTextResult.annotatedString,
130+ activeAnimations = emptyList(),
131+ )
132+ }
129133 }
130- val isPartialText = animatedText.text.length < decoratedTextResult.annotatedString.text.length
131- val underlineSpecs = remember(
132- decoratedLinkRanges,
133- resolvedStyle,
134- contentColor,
135- animatedText,
136- isPartialText,
137- ) {
134+ val animatedText = animatedTextResult.text
135+ val underlineSpecs = remember(decoratedLinkRanges, resolvedStyle, contentColor) {
138136 decoratedLinkRanges.mapNotNull { range ->
139- if (isPartialText && range.end > animatedText.text.length) return @mapNotNull null
140137 val linkStyle = range.linkStyleOverride
141138 ?.invoke(resolvedStyle.linkStyle)
142139 ? : resolvedStyle.linkStyle
143140 val underlineColor = range.underlineColor
144141 ? : linkStyle?.style?.color
145142 ?.takeIf { it.isSpecified }
146143 ? : contentColor
147- val textLength = animatedText.text.length
148- val clampedStart = range.start.coerceIn(0 , textLength)
149- val clampedEnd = range.end.coerceIn(0 , textLength)
150- if (clampedStart >= clampedEnd) return @mapNotNull null
151- val hasLinkAnnotation = animatedText
152- .getLinkAnnotations(clampedStart, clampedEnd)
153- .isNotEmpty()
154- if (! hasLinkAnnotation) return @mapNotNull null
155144 UnderlineSpec (
156145 range = range,
157146 color = underlineColor,
158147 )
159148 }
160149 }
161150 val underlineModifier = if (underlineSpecs.isNotEmpty()) {
151+ val activeAnimations = animatedTextResult.activeAnimations
162152 Modifier .drawWithContent {
163153 drawContent()
164154 val layoutResult = textLayoutResult ? : return @drawWithContent
165155 underlineSpecs.fastForEach { spec ->
156+ if (activeAnimations.any { animation ->
157+ animation.startIndex >= spec.range.start &&
158+ animation.startIndex < spec.range.end &&
159+ animation.alpha < 1f
160+ }
161+ ) {
162+ return @fastForEach
163+ }
166164 drawUnderline(
167165 layoutResult = layoutResult,
168166 start = spec.range.start,
@@ -319,14 +317,19 @@ public class MarkdownAnimationState {
319317 (lastAnimationStartMs - System .currentTimeMillis()).coerceAtLeast(0 ).toInt()
320318}
321319
320+ private data class AnimatedTextResult (
321+ val text : AnnotatedString ,
322+ val activeAnimations : Collection <TextAnimation >,
323+ )
324+
322325@Composable
323326private fun rememberAnimatedText (
324327 annotated : AnnotatedString ,
325328 renderOptions : RichTextRenderOptions ,
326329 contentColor : Color ,
327330 sharedAnimationState : MarkdownAnimationState ,
328331 isLeafText : Boolean ,
329- ): AnnotatedString {
332+ ): AnimatedTextResult {
330333 val coroutineScope = rememberCoroutineScope()
331334 val animations = remember { mutableStateMapOf<Int , TextAnimation >() }
332335 val textToRender = remember { mutableStateOf(AnnotatedString (" " )) }
@@ -398,10 +401,13 @@ private fun rememberAnimatedText(
398401 // the text will just be re-drawn, since the animated alpha state was read only inside
399402 // DynamicSolidColor during the draw phase.
400403 derivedStateOf {
401- textToRender.value.withDynamicColorPhrases(
402- contentColor = contentColor,
403- animations = animations.values,
404- onlyVisible = renderOptions.onlyRenderVisibleText,
404+ AnimatedTextResult (
405+ text = textToRender.value.withDynamicColorPhrases(
406+ contentColor = contentColor,
407+ animations = animations.values,
408+ onlyVisible = renderOptions.onlyRenderVisibleText,
409+ ),
410+ activeAnimations = animations.values.toList(),
405411 )
406412 }
407413 }.value
@@ -461,17 +467,7 @@ private fun AnnotatedString.withDynamicColor(color: Color, alpha: () -> Float):
461467 start = 0 ,
462468 end = length
463469 )
464- val builder = AnnotatedString .Builder (text)
465- subStyles.fastForEach { builder.addStyle(it.item, it.start, it.end) }
466- builder.addStyle(fullStyle.item, fullStyle.start, fullStyle.end)
467- paragraphStyles.fastForEach { builder.addStyle(it.item, it.start, it.end) }
468- getLinkAnnotations(0 , length).fastForEach { annotation ->
469- when (val link = annotation.item) {
470- is LinkAnnotation .Url -> builder.addLink(link, annotation.start, annotation.end)
471- is LinkAnnotation .Clickable -> builder.addLink(link, annotation.start, annotation.end)
472- }
473- }
474- return builder.toAnnotatedString()
470+ return AnnotatedString (text, subStyles + fullStyle)
475471}
476472
477473private fun CharSequence.maybeContainsEmojis (): Boolean {
0 commit comments