Issue
In the past, a kind of animation could be included in the text, in which if the text exceeded the limits, it would automatically scroll horizontally. This was done by including: android:ellipsize"marquee"
, and the result was something similar to the one shown here:
The problem is that in Jetpack Compose I don’t see a way to include that option inside the Composable Text, there is the TextOverflow
that includes the Clip, Ellipsis or Visible options, but I don’t know if there is a way to include or use the “Marquee” option in Jetpack Compose. Is there any way to do it?
Solution
This is not yet supported by Compose, but it’s not too hard to implement. You will need TargetBasedAnimation
, which will update the text offset, and SubcomposeLayout
, which lies under most collections. Inside you can define the size of the text, and also place the second similar Text
, which will appear from the right edge.
@Composable
fun MarqueeText(
text: String,
modifier: Modifier Modifier,
gradientEdgeColor: Color Color.White,
color: Color Color.Unspecified,
fontSize: TextUnit TextUnit.Unspecified,
fontStyle: FontStyle? null,
fontWeight: FontWeight? null,
fontFamily: FontFamily? null,
letterSpacing: TextUnit TextUnit.Unspecified,
textDecoration: TextDecoration? null,
textAlign: TextAlign? null,
lineHeight: TextUnit TextUnit.Unspecified,
overflow: TextOverflow TextOverflow.Clip,
softWrap: Boolean true,
onTextLayout: (TextLayoutResult) -> Unit {},
style: TextStyle LocalTextStyle.current,
) {
val createText @Composable { localModifier: Modifier ->
Text(
text,
textAlign textAlign,
modifier localModifier,
color color,
fontSize fontSize,
fontStyle fontStyle,
fontWeight fontWeight,
fontFamily fontFamily,
letterSpacing letterSpacing,
textDecoration textDecoration,
lineHeight lineHeight,
overflow overflow,
softWrap softWrap,
maxLines 1,
onTextLayout onTextLayout,
style style,
)
}
var offset by remember { mutableStateOf(0) }
val textLayoutInfoState remember { mutableStateOf<TextLayoutInfo?>(null) }
LaunchedEffect(textLayoutInfoState.value) {
val textLayoutInfo textLayoutInfoState.value ?: return@LaunchedEffect
if (textLayoutInfo.textWidth < textLayoutInfo.containerWidth) return@LaunchedEffect
val duration 7500 * textLayoutInfo.textWidth / textLayoutInfo.containerWidth
val delay 1000L
do {
val animation TargetBasedAnimation(
animationSpec infiniteRepeatable(
animation tween(
durationMillis duration,
delayMillis 1000,
easing LinearEasing,
),
repeatMode RepeatMode.Restart
),
typeConverter Int.VectorConverter,
initialValue 0,
targetValue -textLayoutInfo.textWidth
)
val startTime withFrameNanos { it }
do {
val playTime withFrameNanos { it } - startTime
offset (animation.getValueFromNanos(playTime))
} while (!animation.isFinishedFromNanos(playTime))
delay(delay)
} while (true)
}
SubcomposeLayout(
modifier modifier.clipToBounds()
) { constraints ->
val infiniteWidthConstraints constraints.copy(maxWidth Int.MAX_VALUE)
var mainText subcompose(MarqueeLayers.MainText) {
createText(Modifier)
}.first().measure(infiniteWidthConstraints)
var gradient: Placeable? null
var secondPlaceableWithOffset: Pair<Placeable, Int>? null
if (mainText.width < constraints.maxWidth) {
mainText subcompose(MarqueeLayers.SecondaryText) {
createText(Modifier.fillMaxWidth())
}.first().measure(constraints)
textLayoutInfoState.value null
} else {
val spacing constraints.maxWidth * 2 / 3
textLayoutInfoState.value TextLayoutInfo(
textWidth mainText.width + spacing,
containerWidth constraints.maxWidth
)
val secondTextOffset mainText.width + offset + spacing
val secondTextSpace constraints.maxWidth - secondTextOffset
if (secondTextSpace > 0) {
secondPlaceableWithOffset subcompose(MarqueeLayers.SecondaryText) {
createText(Modifier)
}.first().measure(infiniteWidthConstraints) to secondTextOffset
}
gradient subcompose(MarqueeLayers.EdgesGradient) {
Row {
GradientEdge(gradientEdgeColor, Color.Transparent)
Spacer(Modifier.weight(1f))
GradientEdge(Color.Transparent, gradientEdgeColor)
}
}.first().measure(constraints.copy(maxHeight mainText.height))
}
layout(
width constraints.maxWidth,
height mainText.height
) {
mainText.place(offset, 0)
secondPlaceableWithOffset?.let {
it.first.place(it.second, 0)
}
gradient?.place(0, 0)
}
}
}
@Composable
private fun GradientEdge(
startColor: Color, endColor: Color,
) {
Box(
modifier Modifier
.width(10.dp)
.fillMaxHeight()
.background(
brush Brush.horizontalGradient(
0f to startColor, 1f to endColor,
)
)
)
}
private enum class MarqueeLayers { MainText, SecondaryText, EdgesGradient }
private data class TextLayoutInfo(val textWidth: Int, val containerWidth: Int)
Usage:
MarqueeText("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt")
Result:
Answered By – Philip Dukhov