I few weeks ago I was building a budget tracker with Compose and needed a marquee text, or in other words a component that would scroll the text if the text didn’t fit inside its bounds.
Jetpack Compose doesn’t provide something like that so after doing some research, I found this component.
It works in most cases but the code is not that easy to understand. Today we’ll be going over the code to understand how it works and try to improve it.
Understanding the marquee
If the text fits inside its bounds then the
MarqueeText works like a normal
Text component, if it doesn’t, then another
Text component is created to give that cool scroll effect.
Understanding the code
We can see the
MarqueeText component has a lot of fields, it’s almost the same fields a normal
Text component has because the marquee is basically wrapping it. The only difference is
gradientEdgeColor, that defines the color on the edges of the component.
createText lambda is used to create the
Text component, it’s defined as a lambda because multiple instance of the
Text component might be created.
offset variable represents the x offset of the first text component,
textLayoutInfoState is used to store some information about the text and container width.
In the next part we have the code that performs the animation
You might be asking “why was
withFrameNanos used here?” There are other ways to achieve a similar behavior but let’s understand why
withFrameNanos in particular. According to its documentation:
Suspends until a new frame is requested, immediately invokes
onFramewith the frame time in nanoseconds in the calling context of frame dispatch, then resumes with the result from onFrame.
frameTimeNanosshould be used when calculating animation time deltas from frame to frame as it may be normalized to the target time for the frame, not necessarily a direct, “now” value.
As you can see this function suspends until a new frame is requested, meaning that you’ll get a chance to update your animation before a new frame is drawn.
If you have a 60fps display, that means the screen is refreshed 60 times per second. If you update your animation 80 times per seconds, you’ll be wasting resources because some updates will never be drawn. The same thing applies if you draw 15 times per second, the user will notice a janky animation.
withFrameNanos solves that problem by suspending until a new frame is requested so you don’t have to worry about the display frame rate.
The next part of the code uses a
SubcomposeLayout to lay out the components. First it starts by sub composing the main text component and measures it as if there were no width constraints. We need to do this to know whether or not the text will fit inside the constraints.
Then the variables for the gradient and second text component are defined, they’re nullable because they might not be needed if the text fits inside its constraints.
If the text fits inside its constraints then
mainText is updated to fill max width. It also sets
textLayoutInfoState to null so the animation doesn’t run.
If the text doesn’t fit inside its constraints then the gradient and second text components are defined.
Finally the components are laid out
Initially the marquee component looks complex but by analyzing it bit by bit we can see that it’s not that complex.
A few improvements
While doing this analysis I found some things that could be improved in the code. I’ll only mention some of the changes I made, if you want to check out all of them, you can find a link to the source code at the of the article.
LaunchedEffect block there are 2 while loops that look suspicious, the first one is
while(true), it can be removed without affecting anything. The second one is
while (!animation.isFinishedFromNanos(playTime)), as we know the animation never finishes so I changed it to
while (isActive), this returns true while the Coroutine is active.
I also removed the delay, I don’t think that’s needed for a marquee.
We can also change the
MarqueeText component to follow the slot pattern. I created a new component called
Marquee and changed its signature to accept any kind of content.
That way you can pass the
Marquee any content you want, even if it’s not text.
This is the final result
The way the gradient was implemented for this component doesn’t look good in all cases but that’s not something I wanted to dive into. Also I don’t know how the marquee should work in RTL languages so that might require changing some code.
If you want to check out the budget tracker I built using Jetpack Compose, you can find the article here.
You can also find the source code for the
Marquee component here.
See you in my next article 👋