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?
To read value entered in TextField in Android Compose, declare a variable, and assign this variable the value in the TextField whenever there is a change in the value. The same process holds for reading value entered in OutlineTextField composable.
Android Compose – Change Text to Bold To change font weight of Text composable to Bold, in Android Jetpack Compose, pass FontWeight. Bold for the optional fontWeight parameter of Text composable.
A LazyColumn is a vertically scrolling list that only composes and lays out the currently visible items. It's similar to a Recyclerview in the classic Android View system.
In this page, you'll learn about the different side-effect APIs Jetpack Compose offers. Key Term: A side-effect is a change to the state of the app that happens outside the scope of a composable function. State and effect use cases As covered in the Thinking in Compose documentation, composables should be side-effect free.
Marquee is a scrolling piece of text that is displayed either horizontally or vertically. It is used to show some important notice or headlines. It makes app UI much attractive. Note that we are going to use Java as the programming language. A sample GIF is given below to get an idea about what we are going to do in this article.
Key Term: An effect is a composable function that doesn't emit UI and causes side effects to run when a composition completes. Due to the different possibilities effects open up in Compose, they can be easily overused.
You may provide a constant as the key if you just want the effect to execute once when it joins the composition and then dispose it when you leave. This can be done by: DisposableEffect (true) or DisposableEffect (Unit). It’s worth noting that DisposableEffect always necessitates the use of at least one key.
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,
textModifier: 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(textModifier)
}.first().measure(infiniteWidthConstraints)
var gradient: Placeable? = null
var secondPlaceableWithOffset: Pair<Placeable, Int>? = null
if (mainText.width <= constraints.maxWidth) {
mainText = subcompose(MarqueeLayers.SecondaryText) {
createText(textModifier.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(textModifier)
}.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:
Simple solution yet not perfect
val scrollState = rememberScrollState()
var shouldAnimate by remember {
mutableStateOf(true)
}
LaunchedEffect(key1 = shouldAnimated){
scrollState.animateScrollTo(
scrollState.maxValue,
animationSpec = tween(10000, 200, easing = CubicBezierEasing(0f,0f,0f,0f))
)
scrollState.scrollTo(0)
shouldAnimated = !shouldAnimated
}
Text(
text = value,
color = Color.White,
fontSize = 10.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.horizontalScroll(scrollState, false)
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With