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.
The 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.
Next the 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
onFrame
with the frame time in nanoseconds in the calling context of frame dispatch, then resumes with the result from onFrame.
frameTimeNanos
should 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.
Inside the 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 MarqueeText
and Marquee
component here.
See you in my next article 👋
Cover photo by Sven Brandsma on Unsplash