A better underline for Android

Romain Guy
Jun 29, 2016 · 6 min read

Over the past two years, I have regularly come across articles and libraries that attempt to improve how underline text decorations are rendered on the web. The same problem exist on Android: underline text decorations cross descenders. Compare how Android draws underlined text (top) and how it could be drawn instead (bottom):

Image for post
Image for post
Which do you prefer?

While I wholeheartedly approve of these efforts, I have never been fond of the solutions made publicly available. The current state of the art — admittedly forced upon developers by the limitations of CSS — seems to rely on drawing linear gradients and multiple shadows (I have seen up to 12!). These solutions have the undeniable quality of working but the idea of drawing so many shadows, even without blurring them, makes the graphics programmer in me cringe. They also only work on solid color backgrounds.

On a whim, I set out to find other solutions this afternoon that would satisfy the following requirements:

  • Work on older versions of Android
  • Use only standard View and Canvas APIs
  • Do not require overdraw or expensive shadows
  • Work on any background, not just solid colors
  • Do not rely on the ordering of operations in the rendering pipeline (text drawn before/after the underline should not matter)

I do have two solutions to offer and that I have made available on GitHub. One solution works on API level 19 and up and the other one works on API level 1 and up. Or at least it should work on API level 1 and up, I have not exactly tried. I trust the documentation.

You can observe and compare the two methods, called path and region, in the following screenshot:

Image for post
Image for post
Two possible implementations for better underline text decorations on Android

How does it work?

Using paths

Using this API, crafting our underlines becomes trivial. The first step is to get the outline of our text:

mPaint.getTextPath(mText, 0, mText.length(), 0.0f, 0.0f, mOutline);

Note that the resulting path can be used to render the original text using a fill style. We are instead going to use it for further operations.

Image for post
Image for post
Text outline

The next step is to clip the outline with the rectangle representing the underline. This step is not entirely necessary but avoids artifacts and other approximations that could arise in the next step. To do so, we simply use an intersection path operation:

mOutline.op(mUnderline, Path.Op.INTERSECT);

The outline path now only contains the descender bits that cross the underline:

Image for post
Image for post
Only the black regions are part of the path, the rest is drawn for visualization purpose only

All that is left to do is subtract the descender bits from the underline. Before doing so, we must expand the size of the original text to create gaps between the descenders and the underline. This can be achieved by stroking our clipped outline and creating a new fill path:

mStroke.setStyle(Paint.Style.FILL_AND_STROKE);        mStroke.setStrokeWidth(UNDERLINE_CLEAR_GAP);
mStroke.getFillPath(mOutline, strokedOutline);

The stroke width determines how much space you want to leave between a descender and the underline.

Image for post
Image for post
Stroking the clipped outline

The last step is to subtract the stroked, clipped outline from the underline rectangle using another path operation:

mUnderline.op(strokedOutline, Path.Op.DIFFERENCE);

The final underline path can be drawn using a fill paint:

canvas.drawPath(mUnderline, mPaint);

Using regions

What makes regions particularly interesting, is that they offer operations similar to path operations. Two regions can be intersected, subtracted, etc. More importantly, regions have been part of the Android API since the very first version.

The region implementation is almost identical to the path implementation. The major difference lies in when and how the outline path is clipped.

Region underlineRegion = new Region(underlineRect);// Create a region for the text outline and clip
// it with the underline
Region outlineRegion = new Region();
outlineRegion.setPath(mOutline, underlineRegion);
// Extract the resulting region's path, we now have a clipped
// copy of the text outline
mOutline.rewind();
outlineRegion.getBoundaryPath(mOutline);
// Stroke the clipped text and get the result as a fill path
mStroke.getFillPath(mOutline, strokedOutline);
// Create a region from the clipped stroked outline
outlineRegion = new Region();
outlineRegion.setPath(strokedOutline, new Region(mBounds));
// Subtracts the clipped, stroked outline region from the underline
underlineRegion.op(outlineRegion, Region.Op.DIFFERENCE);
// Create a path from the underline region
underlineRegion.getBoundaryPath(mUnderline);

Differences between the two approaches

Regions on the other hand operate on whole pixels and will create clean vertical cuts through the underline (as long as your underline is thin enough). Here is a comparison between the two implementations:

Image for post
Image for post
Top: paths. Bottom: regions. Notice the slant? If not, you should. Look harder.

Should I use this in production?

If you were to try and use this code in your application, and I must admit I’d love to see an implementation for spans, I would encourage you to at least cache the final fill path. Since they depend only on the typeface, font size and string, a cache should be fairly trivial to implement.

In addition, the two implementations described in this article are understandably restricted by the public SDK APIs. I have a few ideas on how this effect could be achieved more efficiently if it were to be implemented directly into the Android framework.

The region variant could for instance be optimized by rendering the region itself, without going back to a path (which can cause software rasterization and GPU texture updates). Regions are internally represented as collections of rectangles and it would be trivial for the rendering pipeline to draw a series of lines or rectangles instead of rasterizing a path.

Do you want to know more about text on Android? Learn how Android’s hardware accelerated font renderer works.

Get the source of the demo on GitHub.

Android Developers

The official Android Developers publication on Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store