We recently switched the font used in our apps at Square from system default (Roboto on Android) to our own in-house font Square Market. This came with a whole slew of pleasent visual variants, but it didn't have a monospaced version or any tabular digits, both of which are quite useful for passwords and rows of monetary amounts respectively. The idea arose: could we utilize a part of the Android platform to address these two shortcomings? But of course.
I found a great jumping off point from Pierre-Yves Ricau, a fellow Android developer at Square, who pointed me at a brand new blog post from Flavien Laurent titled Spans, a Powerful Concept.. The post has a fantastic breakdown on the many different types of Spans that come out of the box with Android and the various different Span base classes and their baggage; from a thorough reading and some additional investigation it was clear that a
MetricAffectingSpan was going a good jumping off point. Unfortunately, this class only gave you access to the
TextPaint; it didn't seem to offer the level of granularity that would be needed to shift characters around with strange spacing. Cue
ReplacementSpan which fit all my needs nicely with direct access to a
Armed with the the right
Span base class and pretty simple requirements; the code basically wrote itself.
ReplacementSpan's four methods, two are commented to say literally "this method does nothing" leaving two remaining methods for the real meat of our spans,
This method should return how wide the given subset of
text will appear when painted.
This method is in charge of actually painting the given subset of
text onto the
canvas in the proper way.
MonospaceSpan, the goal was quite simple: all characters should have the same allotted width on the canvas, and each character should be drawn centered. When used on a password field, this would make all the letters transition from characters to masking dots in a manner such that all the dots would be equally spaced apart regardless of the width of the individual letters themselves.
After looking at a few other potential usecases outside of password spanning, a couple constructors were deemed useful: one to use the widest character in the spanned string to for the monospace width, one to use the widest character in a given library string as the monospace width, and a no-args version that uses the widest of either 'M' or 'W' as the given monospace width.
getSize() method is pretty straightforward here. We use a simple for-loop to find the widest character in our substring to use as our per-character monospace width. Our total substring width is then simply this monospace width multiplied by the number of characters in our subtring. We also need to set the
paint's font metrics to the given
fm values or things get a bit out of whack (and no, that's not a type, the method really is called
Lastly, we need to actually paint the given letters onto our
canvas. Same as with
getSize(), we want to obtain what the monospace width should be for our subset of characters to draw, then we use simple math to center each letter within its own monospace-width bit of the canvas.
The goal of
TabularSpan was to have multiple buckets of monospaced characters; in the example of our usecase of formatting money ammounts ($5,010.77), all the digit characters should have the same width X and all the delimiter characters like periods and commas should have the same width Z (any character not defined in these two groups will have its standard width). This ensures that when these values are right aligned, all the decimal points and commas will line up vertically for easy visual parsing. You can easily imagine how this could be useful for lots of IP addresses, phone numbers, ID numbers, or other numeric data.
Two constructors seemed to cover ours needs, but it would be very straightfoward to extend this to more buckets of tabularity. The no-args constructor uses standard number digits as the numeral group and both comma and period as the delimiter group; alternatively you can specify what characters those two groups should consist of.
MonospaceSpan, we need to get the widest character to use as the monospace width, but here we have to do it for both the delimiter characters and the numeral characters. Once we have those widths, summing up the total width of this span is as easy as looping over the characters and adding the appropriate width to our total.
draw() method is basically identical to the
MonospaceSpan as well, with the sole difference that there is more than one bucket of monospacing to use when "centering" each letter in its bit of the canvas.
From what I can tell (at least post-Honeycomb), we could safely cache the text between
draw() in both spans and keep the per-character width data for
TabularSpan. This would save us from having to traverse the text again in
draw(), but for the size of these spans and the the sake of simplicty, I've left the spans as they are.
As a result of using ReplacementSpan, I have yet to find a way to gracefully handle being multiline. As a password field or a money formatter, being restricted to a single line of text is a non-issue; nonetheless, this is a somewhat awkard flaw for a general-use text Span. Additionally, while I haven't tested this with RTL langauges, it seems pretty obvious that this would draw the characters LTR. This should be fairly easy to fix with something like
getLayoutDirectionFromLocale, but it's API level 17, so no rush to get that implemented for us now.
And that's pretty much all there is to it.
ReplacementSpan is a really powerful class that gives a pretty incredible level of control over text formatting, keep it in mind next time you come up against some sort of odd formatting needs.
I've posted both
TabularSpan to a tiny little repo here; there's a small sample activity that will demonstrate them as well. Pull requests welcome!