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
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.