I drew lines in a TextView using text Spans
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.
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’.
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 inTextView
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 usingLayout
s 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 Drawable
s.
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:
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.
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!