Skip to content

Marquee with Jetpack Compose

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.

Marquee 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