Pixel Perfect: Creating a Retro Button with Jetpack Compose

Daniela Romano
4 min readNov 8, 2024

--

How can we give our app an old-school look? The simplest methods would be to use a limited color palette (that unforgettable green of the first Game Boy?), a monospace font, and… a pixelated style for the buttons.

Obviously, since they are all straight lines, drawing it won’t be difficult; it just takes some calculations to determine the angles, taking into account the thickness of our “pixel” and the radius of the corner. In my attempts, I always drew sharp corners, let’s be honest, it’s simpler… But the result satisfied me enough.

Let’s break down the work into steps. First of all, let’s start with the side segments. Since all the components of this drawing are rectangles, let’s create the Segment class and the drawSegment function to simplify the code.

Let’s set a pixelSize (4.dp seems appropriate to me) and a cornerSize.

So, what does this code do? It draws horizontal and vertical lines, removing the necessary space for the corner (which is equal to cornerSize * pixelSize).

Now, the most complicated part: defining the corner segments, cyclically, based on the corner size.

These two loops work together to iterate through a grid of positions within the corner areas of the rectangle.

cornerSize determines how many squares you want in each corner (e.g., if cornerSize is 3, you’ll have a 3x3 grid of squares in each corner).

i and j represent the row and column positions within this grid.

`if (i + j == cornerSize)` checks if the sum of the row (i) and column (j) positions equals cornerSize. If true, it means we are on a diagonal position where a square should be drawn.

So it calculates the position of each square using i, j, pixelSize, and the overall size of the square (so it’s always pixelSize x pixelSize).

Ok, now we have the complete border of the button, we can draw everything together using drawSegments.

That was easy, no? Now we just need to add some depth to the button. We will do more or less the same, for highlight and shadow.

Let’s start with the highlight. It is structured with a top line and a top-left corner just between and right of the border

See? It’s the same as before but a little shifted.

Spoiler alert: it’s the same for the shadow.

Same as before, but a “pixel” to the left and a “pixel” to the top.

Last but not least, the background. This one is my least favorite. I need to follow the whole border with a line, of course we need the (more or less) same loop for each corner.

Now we have 4 different draws. We need to add some color.

The rule is this:

Darkest color for border

Darker color for shadow

Medium color for background

Lighter color for highlight

Spoiler: Lightest color for extra spice

Ok, not a great spoiler. The draw is ok for a static component, for example a container, a box… but I wanted a button.

A button needs a pressed state, otherwise user won’t understand if something is happening.

So we need a pressed state. This is just a color game. If the button is pressed, colors change.

Shadow becomes highlight and vice versa, background becomes lighter. I swear, it is enough.

I mean, you can do more, you can create a color set for a disabled button, you should add some padding on the inside so that the text is inside the border, you can create custom widget, for buttons, dialogs, bottom sheets..

But I leave that to you, otherwise where is the fun?

I leave you the full code for the PixelContainer, a bit cleaner.

https://gist.github.com/kiwi4747/4fe0590baa046ed3eff7e52e270f3824

(Hey, if you have the patience to write the loop for the rounded corner, let me know!)

--

--

No responses yet