I drew lines in a TextView using text Spans

Efeturi Money
4 min readDec 14, 2017

--

Saket Narayan recently tweeted the link to his slides for his talk on Text Spans at Droidcon San Fransisco and it reminded me of a time I drew lines in a TextView using text spans. I thought I should share how.

Great slides. I believe the video recording is out too!

A while ago I had to implement an interesting piece of UI while working on Haute App. Please pay attention to the list of ‘features’.

Each ‘feature’ is delimitted with a bullet and underline

I’d normally just throw in a ListView with an Adapter or dynamically generate Views and add to a LinearLayout container and call it a day but I wanted to see just how far I could go with text spans. I was gonna use a single TextView and spans to display the entire feature list, including the underline.

Spans are used to attach mark-up or styling information to text in a TextView and are roughly divided into two main classes: CharacterStyle spans that affect character-level text formatting and ParagraphStyle spans that affect paragraph-level text formatting. These are explained in more detail in Flavien Laurent’s excellent article on spans which I recommend you read if you haven’t.

Spans are used by android.text.Layout and it’s subclasses when rendering text in TextView and it’s subclasses. It’s a great API and if you are doing advanced custom text drawing, you can take full advantage of text spans by usingLayouts to draw.

The Bullets

Thankfully, Android provides a BulletSpan, a subclass of ParagraphStyle. This BulletSpan is a paragraph-level span that adds a bullet and indents the entire paragraph it is set on. Unfortunately, however, for my application, the platformBulletSpan hardcoded the bullet radius to 3 (px?) and that did not work for me. As a workaround, I simply duplicated BulletSpan in my own package (taking out all the parcelable stuff) and hardcoded my own radius into it. See theBulletSpan documentation / source for more info.

The Line (<hr>)

Drawing the line was more challenging because of the lack of material on this area of spans. First thing I did was try to find a framework span that would give me the desired effect. Although I didn’t find a special ‘LineSpan’ class, I did find an ImageSpan class and while the name is a tad misleading, ImageSpan is a ReplacementSpan that replaces text with Drawables.

The ReplacementSpan interface has two main callbacks: getSize for retrieving the replacement size and a draw method for drawing the replacement on a canvas. The solution seemed much closer than before and I just had to create a drawable that drew a line which I did:

Not a very customisable line drawable

This didn’t quite work how I thought it would as the line was never drawn and the problem was in how DynamicDrawableSpan, ImageSpan's immediate superclass, measures the replacement size. DynamicDrawableSpan assumes the drawable would have determined its bounds prior to calling getSize and tries to calculate the replacement size using the bounds of the drawable. Unfortunately, our LineDrawable above has zero bounds initially and is therefore not effectively drawn on the screen. Another challenge is that the length of the line is dependent on the length of the TextView which is not readily available when instantiating the LineDrawable.

I couldn’t find a suitable hack short of passing the TextView to the span at creation and reading the width off of it, which was a really ugly solution. Fortunately, the TextView canvas is provided as an argument to the draw callback of ReplacementSpan and is a good handle to getting the currentTextView width. The solution was to update the drawable bounds with the currentTextView width just before it is drawn.

To do that, I copied the implementation of DynamicDrawableSpan and updated the implementation of the getSize and draw methods to suit my use case.

The caveat, though, is that because the size of the TextView width is not available when determining the replacement size and we instead return a size of zero, the TextView will not consider the replacement when laying out its text.

The TextView will draw its text like the line is not there

We can simply work around this by wrapping the text to be replaced with newline characters.

Putting It Together

We put these together in our ViewPager adapter implementation:

As you can see in the description strings, I use the ~ (tilde) character as a marker for the line we want to draw, wrapping it with two new line characters. This results in the line drawable being drawn in its own text line. I also use regex patterns to find the regions of text we’re interested in and set the spans on them.

Why did I do this?

While this was initially a thought experiment to determine if it was possible to draw lines in a TextView using spans, there are still some advantages to using spans for this purpose. For one, layout is faster and more performant as we have eliminated the need for multiple Views and ViewGroups we would’ve needed otherwise. Also, its a lot more contextual usage of text spans as opposed to Views, imo.

Thanks for reading!

--

--