Color Wheel — Efficient drawing with shaders

Yaroslav Shevchuk
4 min readApr 1, 2018

--

Recently I came across a series of articles on custom color wheel widget development. Before reading this article, I strongly recommend you getting acquainted at least with the first article in the series to understand the context.

The author does a really good job explaining how to calculate a color for each pixel in a wheel, but I was kind of disappointed with the computational cost of the proposed solution. In this short article I will show an approach to create an HSV color space approximation in less than a hundred lines of code. At least to my eye, the resulting image is good enough to be used in real projects (please, correct me if I am wrong).

To begin with, lets read about HSV in Wikipedia:

In both geometries, the additive primary and secondary colors — red, yellow, green, cyan, blue and magenta — and linear mixtures between adjacent pairs of them, sometimes called pure colors, are arranged around the outside edge of the cylinder with saturation 1.

So, we need to draw a circle with 6 colors, as enumerated by Wikipedia, and their mixtures. Luckily Android has a special class right for the task: SweepGradient. It’s a Shader class implementation which takes a list of colors and their positions on a circle as its constructor arguments.

When our color wheel size has already been computed, we create a shader:

Shader sweepShader = new SweepGradient(centerX, centerY,
new int[] {
Color.RED, Color.MAGENTA, Color.BLUE, Color.CYAN,
Color.GREEN, Color.YELLOW, Color.RED},
new float[] {
0.000f, 0.166f, 0.333f, 0.499f,
0.666f, 0.833f, 0.999f});
huePaint.setShader(sweepShader);

These esoteric float values passed to shader’s constructor denote positions of supplied colors. They are uniformly distributed across the circle with a step computed as:

(360 / numberOfColors) / 360

If we then use this paint to draw a circle, here is what we’ll get:

Well, quite far from the goal yet. To put it simple, on the target image hues become “whiter” as they approach the center. To achieve a similar effect we can draw a white-to-transparent gradient on top of our wheel. Again, shaders to the rescue:

Shader satShader = new RadialGradient(centerX, centerY, radius,
Color.WHITE, 0x00FFFFFF,
Shader.TileMode.CLAMP);
saturationPaint.setShader(satShader);

Looks much better now:

The only thing left is to make wheel brightness configurable. Nothing complicated here also, we will just use a brightness value to calculate alpha of a black overlay to be drawn on top of our wheel:

brightnessOverlayPaint.setColor(Color.BLACK);
brightnessOverlayPaint.setAlpha(255 - brightness);

A video of the final result is available on YouTube:

And the source code can be found in this GitHub repository.

In his second article, Mark Allison suggests drawing a wheel using RenderScript. Is our solution more efficient than that? I claim yes. We don’t need to allocate an extra bitmap and do expensive data transfers: when RenderScript is used, input data should be first passed to a GPU, after the computation completes the result goes back to a bitmap in the main memory, and then this bitmap again travels to the GPU to get to the screen. I oversimplified the process but my point is — data transfer is expensive. When drawing with shaders, we simply tell the GPU what algorithm to use to draw an image which then goes right to the display.

Does performance really matter? The short answer is yes. Color pickers are often displayed as a part of a dialog or a bottomsheet, which means that the view may be recreated several times, every time generating a bitmap. Also an expensive computation constantly happens when user interacts with a brightness slider. Without extra multithreading, frames will be dropped. And to quote official docs:

To ensure that a user’s interaction with your app is smooth, your app should render frames in under 16ms to achieve 60 frames per second (why 60fps?). If your app suffers from slow UI rendering, then the system is forced to skip frames and the user will perceive stuttering in your app. We call this jank.

Does my solution have any cons? I think the only disadvantage is drawing impreciseness, but is preciseness a requirement at all? For precise color selection mixers and text input exist, while color wheels are used to choose a color which is approximately what you want. Users don’t see individual pixels and they can’t point to a particular pixel with a finger. To my mind, as long as the resulting image can hardly be distinguished from an ideal drawing, implementation simplicity and performance matter much more than accuracy.

Suggested readings:

  1. A guide to shaders and color filters provided by Android SDK.
  2. A rather old, but very interesting read on UI toolkit internals.

--

--