AnnotationSpan / Spans / Text

AnnotationSpans – Part 2

I recently read a blog post on the Android Developers blog by Florina Muntenescu from which I learned about a really interesting technique for applying spans to text. I was even more surprised to learn from Florina that the API in question has been there since API 1. As I was unaware of this incredibly powerful API, I’m guessing that others may also be blissfully unaware of it, so we’ll take a look at how to use Annotation in this series.

Previously we looked at the basics of how to use annotation spans, and provided a simple example where we hooked up a single bold formatter. Florina’s article contains a really useful pattern for applying custom fonts to specific annotation spans and I won’t reproduce that here just for the sake of it. Instead I’ll re-visit a previous Styling Android post where we looked at how to apply superscript to ordinal numbers using regular expressions to match the ordinals in order to determine the range to which we should apply the span. Using annotation spans is actually a much cleaner and more performant solution because we are not performing possibly multiple regular expression lookups on our string, and we also have fine control within our string resources because we can precisely position the annotations within different translations.

Before we dive in to the implementation, let’s first do a little bit of house keeping to make life easier. The current code works well for matching a simple annotation key / value pair, but isn’t easily extensible. We can improve this by having a match on the annotation key, and then defining a function to handle specific values:

private suspend fun processAnnotations(text: CharSequence?): CharSequence? {
    return if (text is SpannedString) {
        withContext(Dispatchers.IO) {
            val spannableStringBuilder = SpannableStringBuilder(text)
            text.getSpans(0, text.length, Annotation::class.java)
                    .filter { it.key == "format" }
                    .forEach { annotation ->
                        text.processFormatAnnotations(annotation, spannableStringBuilder)
                    }
            spannableStringBuilder.toSpannable()
        }
    } else {
        text
    }
}

private fun SpannedString.processFormatAnnotations(annotation: Annotation, output: SpannableStringBuilder) {
    val start: Int = getSpanStart(annotation)
    val end: Int = getSpanEnd(annotation)
    when (annotation.value) {
        "bold" -> output[start..end] = StyleSpan(Typeface.BOLD)
    }
}

The value we get from this simple change can be demonstrated by how easy it now is to add italic support:

private fun SpannedString.processFormatAnnotations(annotation: Annotation, output: SpannableStringBuilder) {
    val start: Int = getSpanStart(annotation)
    val end: Int = getSpanEnd(annotation)
    when (annotation.value) {
        "bold" -> output[start..end] = StyleSpan(Typeface.BOLD)
        "italic" -> output[start..end] = StyleSpan(Typeface.ITALIC)
    }
}

Now it is pretty easy to add the superscript annotation. In the previous article much of the explanation was regarding the regular expression used to match the ordinals that we wanted to apply the superscript formatting to, and the formatting itself was relatively straightforward. Essentially we need to apply two distinct spans. The first is a SuperscriptSpan which raises the baseline of the text. However doing this alone does not change the text size, so we also applied a RelativeSizeSpan to make the text smaller as well. We can add this in just a few lines of code:

private fun SpannedString.processFormatAnnotations(annotation: Annotation, output: SpannableStringBuilder) {
    val start: Int = getSpanStart(annotation)
    val end: Int = getSpanEnd(annotation)
    when (annotation.value) {
        "bold" -> output[start..end] = StyleSpan(Typeface.BOLD)
        "italic" -> output[start..end] = StyleSpan(Typeface.ITALIC)
        "superscript" -> {
            output[start..end] = SuperscriptSpan()
            output[start..end] = RelativeSizeSpan(RELATIVE_SIZE)
        }
    }
}

We can now apply this by including annotations in our string resources:

<string name="ordinals">The 5<annotation format="superscript">th</annotation> floor or the 3<annotation format="superscript">rd</annotation> page</string>

Not only do we have much finer-grained control over where the superscript is positioned, we no longer require quite complex regular expressions to try to debug if things don’t work as expected – the behaviour is much more predictable. Moreover for some language translations the regular expression may be further complicated where languages support grammatical genders. For example, in in Italian the above string would translate to “Il 5º piano o la 3ª pagina”, so our regular expression would need to account for that. However using the annotation approach, this becomes much simpler:

<string name="ordinals">Il 5<annotation format="superscript">o</annotation> piano o la 3<annotation format="superscript">a</annotation> pagina</string>

Hopefully this demonstrates how much more flexible using annotation spans can be that using other techniques to match specific substrings. Also, because it is part of the string resource itself, it makes it much easier to understand the code when you come back to look at it in the future, so results in much more maintainable code.

One thing with mentioning is when you are loading strings from resources in Java or Kotlin. Normally we might use Resources#getString() to achieve this, but this will only load the String itself without the annotations. Instead we much use Resources#getText() which returns a CharSequence (actually a SpannedString which implements CharSequence) containing the annotation that we can match on using the techniques described previously.

We can see all of this in action if we run the app:

While we have shown how to use annotation spans to easily format strings, but in the next article we’ll take a look at how we can effectively use this throughout our app with minimal effort.

Once again, many thanks to Florina for inspiring this series, and also for Sebastiano Poggi for checking my Italian translations!

The source code for this article is available here.

© 2018, Mark Allison. All rights reserved.

Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

3 Comments

  1. Very interesting thanks! Is there any way these annotation spans can be processed on the fly (by the TextView code that parses the Spanned, for example) rather than introducing this intermediate step? One thing that’s not so neat here is that we are left with both the annotation span and the equivalent style spans together (i.e. redundant information). Doing this on the fly instead, would avoid that.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.