AnnotationSpan / Spans / Text

AnnotationSpans – Part 1

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.

While Florina’s post covers how to use Annotations extremely well, I’m going to be building a little upon that so I’ll cover the basics once again to contextualise things for what is to follow. Many people will be familiar with how we can format text using a subset of HTML markup within our string resources, but we can also embed custom markup within our string resources and these enable us to apply our own spans to the text which is delimited using these annotations. This is a concept which will be easier to grasp through example rather than dry explanation, so let’s dive straight in to an example. Let’s define a string resource as follows:

<string name="annotated_text">This is a string containing an <annotation format="bold">annotation</annotation> which we can use to <annotation format="italic">style</annotation> the substrings</string>

This is a string resource containing the text ‘This is a string containing an annotation which we can use to style the substrings’, but there are a couple of <annotation> tags surrounding the words ‘annotation’ and ‘style’. Each of these annotations has a different attribute: the first has an attribute name of format which has a value of ‘bold‘; and the second also has an attribute named format, yet its value is ‘italic‘. The name and values of these attributes are not dictated by the annotation itself and we can use whatever we like here – we’ll cover this in greater depth later on.

We can now use this string resource in a TextView:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <TextView
    android:id="@+id/annotated_text"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_margin="16dp"
    android:text="@string/annotated_text"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

If we were to run this as-is, then the annotation markup will have no effect, they are merely placeholders which will not have any direct effect on how the text is displayed. The power of these annotations is that they enable us to provide specific formatting on the sections of the text which is delimited by the annotation elements. This is particularly useful in internationalised strings where different translations may require the markup in different locations within the string.

To actually make use of this requires only a small amount of code (partly thanks to some of the language features of Kotlin):

class MainActivity : AppCompatActivity(), CoroutineScope {

    private lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        job = Job()

        launch {
            annotated_text.text = processAnnotations(annotated_text.text)
        }
    }

    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" && it.value == "bold" }
                        .forEach { annotation ->
                            spannableStringBuilder[text.getSpanStart(annotation)..text.getSpanEnd(annotation)] =
                                    StyleSpan(Typeface.BOLD)
                        }
                spannableStringBuilder.toSpannable()
            }
        } else {
            text
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }
}

All of the key code is performed inside the processAnnotations function, and I have marked this as a suspending function to force it to be used inside a coroutine. This is because performing searches on large strings may be something that we don’t want to be doing on the main thread, so calling this function from inside a coroutine on the Dispatchers.Main context will not risk us blocking the main thread.

We pass the current text from the TextView as an argument to this function, and perform an initial check that it is an instance of SpannedString (which it will be if there are annotations in the string resource – that’s how the framework will construct the object that’s read from the string resource. If there are no annotations, then it may not necessarily be passed as a SpannedString instance, so this check will just return the argument if it is not a SpannedString.

If it is a SpannedString then we run an async coroutine using an IO context as the code within the lambda may take some time to run. First we create a SpannableStringBuilder which clones the SpannedString. We next perform a search of the SpannedString for all spans which are of type android.text.Annotation. This will return an array of Annotation instances each of which represents an <annotation> element within the string. So in our example, this will return two objects.

Next we use the Kotlin filter property on the collection to limit the matches to only those which have a key of ‘format’ and a value of ‘bold’. This comes back to what we touched upon earlier about how the attributes can be any key name and value that we like – these are no specific values built in to the annotation element itself, but we can use them to group and specify specific span types that we want to apply. In this example I have chosen to only match one of the annotation types that we used. In a real world use-case you’d want to handle all of the supported types here, but in this case I have only handled the ‘bold’ format attribute. When this is matched we apply a bold StyleSpan to the SpannableStringBuilder using a KTX extension function which wraps the setSpan() method of SpannableStringBuilder.

If we now run this we can see that the word ‘annotation’ has been emboldened while the rest of the string, including the word ‘style’ which was wrapped inside an annotation that we didn’t handle, have the default text style:

Hopefully the power that this gives us to apply custom spans to our string resources is fairly obvious, but we could have achieved the same thing by empedding a <b> tag in our string resource. In the next article we’ll take a look at some alternative span types that we can apply using this technique.

Once again a massive thank you to Florina for making me aware of this incredibly powerful API.

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.

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.