Ticker Board in Jetpack Compose

How to create an old fashioned Ticker board to add some distinctiveness to your app

Introduction

In today's article we will learn how to create a ticker board, reminiscent of the boards that used to adorn train stations and airports, and that can still be seen in some of those today. The GIF below shows what we will end up implementing today:

Let's get started.

The issue with text

Working with text can be a bit challenging because font families have to account for the different sizes (width and height) or each letter. We can simplify this a bit by using a monospaced font, which will ensure all letters have the same width. but height can be somewhat of a challenge. To illustrate this, I'll draw a simple letter in a Box container, and I'll draw a line across the middle of the Box, the code is shown here:

This is very simple, just a Box containing a 1 letter Text with monospaced font. We use the drawBehind modifier extension to draw a red line in the middle of the box. If we run, we get this result

We can clearly see that there is significantly more body of the letter below the middle line than above it. If we were to use this letter as is for our ticker, the Ticker would be misaligned -  when the top half folds over the bottom it would be offset. We'll need to fix that.

Ascend, Descend, Baseline

Before we can fix this, we need to understand the different attributes of text, a line of text has these properties: top, ascend, baseline, descend and bottom. The image below illustrates them (excuse my poor artistic skills, I'm an engineer, not a designer):

To make sure that the letter is centered in the box, with as much body above the middle line as below it, we will need to get these text properties and then shift the letter in order to center it.

The text properties can be obtained from the Text composable, we can use the onTextLayout parameter to pass a lambda that will be called when the text is ready to be laid out, and here we can calculate these properties. This is how we can do this:

Now that we have this, we have to figure out how to offset the letter so that it becomes centered in its box. If we look at the drawing of the different properties above, we can determine that what we need to do is ensure that the distance from the top to the ascend is the same as from the baseline to the bottom, so the amount we need to shift the letter by can be calculated as shown in this formula


val delta = ((bottom - baseline) - (ascent - top)) / 2


Once we have this, we can apply an offset to the letter, using the Offset Modifier. Let's see the code:

If we display this, next to the original text, we can see the difference:

Now that we have our centered letter, we can proceed to the next step on our Ticker.

A tale of two halves

Now we have the basic block for our Ticker, a centered text composable. Next we have build the 2 halves of the Ticker, the top half that folds down, and the bottom half. Because we need to fold only half of the letter, and we want to have only the other half showing below it, we need to find a way to split our letter into 2 distinct parts, so that we can manipulate them individually. There are probably multiple ways to tackle this problem, but the solution I've come up with involves using a Layout composable. This is a basic building block that allows us to measure the content and then place it within the constraints of the Layout composable. To render only the top half of the letter, we will measure it as we would always do, but when it comes to laying it out, we will display only half and apply a truncation - we will do this by halving the height we specify as required to draw the content. Let's see the code, it will be easier to explain that way:

If we now wrap our CenteredTextView in this container, we get this result:

And we can do the same with the bottom half, but for the bottom half we will need to offset the child by its height, so that only the bottom shows. This is how we would do it:

And this is the result:

Laying out the pieces

The 3D model

The Ticker needs to be built from 3 pieces:

The tricky part is the top half, which needs to bend over the bottom half, and change what it displays once it crosses the half mark threshold of the animation.

It may be easier to visualize this if we see the 3 elements overlaid next to a 3D model of where they sit relative to each other:

On the left side we have the front view of the ticker, while on the right side we can see the 3 elements, the background with a B, the top half with a partially folded 1/2 top A or 1/2 top B, and the 1/2 bottom A.

So we want to

We can achieve this with this incomplete Ticker composable:

I'll note that there is an alternative way to go about this, we could have a background letter made of two halves, a next letter top half and a bottom half current letter, then the folding half letter overlaid on top of that; this alternative solution does not require setting the zIndex. The final implementation linked at the end of this article uses this approach.

The Alphabet

We will define an Alphabet of supported letters, as we have a finite set of letters we can render in the Ticker, our alphabet will consist of these letters


 ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789•


The last letter on this alphabet is a "catch-all", if we receive a letter outside the alphabet we will default to displaying this dot.

The Animation

Now we have to decide how to animate the Ticker. To do so, we will use an animatable object from Jetpack Compose, which allows us to animate a float between 2 values. The values that this animatable will represent are the index in our alphabet of the letter to display in the Ticker. So, for our alphabet, the letter A will be index 0, letter B will be index 1 and so on. Float values between 0f and 1f will represent the folding state of the current letter, as we move from A to B. In other words, a value of 0f means the letter A is fully displayed, 0.5f means the letter A is folded at 90° (perpendicular to the viewer), and a value of 1f means we are now on the letter B.

We want to kick the animation every time a letter changes, so we will use a LaunchedEffect with the key being the letter to display. To determine how to run the animation, we need to calculate these values:

The value currently displayed is easy, it's just the current value of the animation, truncated to an Int.

The value we want to animate to is not just the index of the letter we want to display - if we are on letter Z (index 25) and we want to go to letter C (index 2), we can't animate from 25 to 2 because this would run our animation backwards (the cards will flip upwards instead of downwards), so if the target index is lower than the current index, we need to add the size of the alphabet, so that we loop over all the characters. In other words, to go from Z to C we would actually go from Z to • and then start over at the beginning, from A to C.

The animation duration will depend on how many letters we need to tick over. We can't use a fixed animation duration, otherwise going from A to B (1 tick) would take as long as going from A to Z (25 ticks), which would look odd. So we will define a base unit for a single tick, and then the animation duration will be the number of ticks to execute, times the base tick unit of time.

The careful reader may notice that, as we keep ticking over letters, the indices will keep moving upwards as we need to add the size of the Alphabet for an index that is lower than the current index so, to avoid precision issues with large floats we will reset the indices once the animation has completed.

Let's look at the code now, it will all become much clearer. First, we will abstract the Alphabet logic to a class:

Now that we have our alphabet, we can create the Ticker animation:

Let's have a look at the changes:

And this is pretty much it, our Ticker is complete. There is some additional clean-up we can do, like extracting the state to a state holder class, but that's just some refactoring that does not change the principle of the Ticker. The refactored code is linked at the end of this article.

Rows and Boards

Now we have a single Ticker, but we may want to display more than a single letter, so we can build a Row of tickers, and a Board of tickers. Let's start with the row:

This is the beauty of Jetpack Compose, once you have a basic block (the Ticker in our case), it's very easy to compose it to build larger, more complex pieces with just a few lines of code,

And just like we built a Ticker row from a set of Tickers, we can build a Ticker board from a set of Ticker rows:

Here we can see the board in action:

The full code, with some clean-up and refactor, is available on this gist.