Spans / Text

Custom Colour Spans

Regular readers of Styling Android should know that I’m a huge fan of Spans and believe that a good understanding of Spans is essential in order to get the best out of TextView. That said sometimes just doing simple things, such as simply changing the text colour, can seem a little awkward. In this article we’ll look at how to roll your own Span implementations, and see how easy it can be to utilise custom Spans.

Before we begin, I’m going to have a small rant. The inspiration for this article was inheriting some code which implemented text styling using one of my least favourite classes android.text.Html. The class itself isn’t the problem, after all it works by parsing html markup in a text string and generates the relevant Spans for you. However my experience is that it encourages hacky code which is difficult to maintain. Often code using the Html class will require re-writing to actually implement things directly using Spans in order to meet changing requirements.

An example may help to illustrate the problem. Consider the case where part of the text of a TextView needs to be a different colour to the rest of the string. This can easily be achieved with the Html class:

Spanned text = Html.fromHtml("One word should be <font color='16711680'>red</font>");
textView.setText(text);

The first issue that I have with this is that it looks fugly. Embedding HTML markup within strings in Java code is an immediate code smell simply because there are the tools available to actually do it more efficiently. While the strings could be moved in to a string resource, often this can not happen if the actual colour value needs to be determined at run time. We end up having to perform even fuglier string concatenation or we need use formatted string resources to generate the appropriate HTML string which we then parse using the Html class. A much bigger issue is that while the fromHtml() method of Html will allow us to specify colour resources (including colour state list) within the markup, it will only look for them within the ‘android‘ package, so we’re constrained to system resources, and we cannot use our own resources. This poses something of a problem if we need to change the text colour when the state of the parent TextView changes.

I’ve explained one use-case which illustrates why Html is bad, so how should it be done? The usual way to change text colour using Spans is to use TextAppearanceSpan, but this usually requires us to specify other things such as the text appearance, or typeface etc. In the above example we’re only interested in changing the text colour of a part of the string, so TextAppearanceSpan is a little overkill. Another option would be to use ForegroundColorSpan but this suffers from the same problem of not supporting colour state list.

But we can really easily create our own custom Span which does precisely what we need (and we can then reuse it throughout the app).

Let’s start with a simple custom span which will simply changes the colour of the text to which it is applied. We could use ForegroundColorSpan here instead (and I certainly would in production code), but I’ve elected to create a custom one purely to help explain the mechanics of how to write our own.:

class StaticColourSpan extends CharacterStyle {
    private final int colour;

    public StaticColourSpan(int colour) {
        super();
        this.colour = colour;
    }

    @Override
    public void updateDrawState(TextPaint tp) {
        tp.setColor(colour);
    }
}

How simple is that? The constructor takes a colour value. Because we extend CharacterStyle, we are required to implement updateDrawState(). This method will be called before onDraw() for the text and allows us to modify the Paint object which will be used to render the text. So all we need to do is set the colour of the Paint object and everything is good.

Now some of you have probably already realised that this does not solve the use-case of the text changing colour when the TextView state changes. But we can create another Span which does precisely this:

class ColourStateListSpan extends CharacterStyle {
    private final ColorStateList colorStateList;

    public ColourStateListSpan(ColorStateList colorStateList) {
        super();
        this.colorStateList = colorStateList;
    }

    @Override
    public void updateDrawState(TextPaint tp) {
        tp.setColor(colorStateList.getColorForState(tp.drawableState, 0));
    }
}

This is pretty similar, the differences are that the constructor takes a ColorStateList rather than a raw colour value, and that in updateDrawSate() we look up the appropriate colour from the ColorStateList depending on the state which we obtain from the TextPaint object. updateDrawState() will be called whenever the TextView is redrawn, therefore the control state will be know at this point.

The next thing to consider is that we really don’t want to be loading ColorStateList objects in order to call this, but we can easily create a factory method which will take a resource identifier and load the appropriate span for us depending on the resource type:

public abstract class TextColourSpan extends CharacterStyle {
    public static TextColourSpan newInstance(Context context, int resourceId) {
        Resources resources = context.getResources();
        ColorStateList colorStateList = resources.getColorStateList(resourceId);
        if (colorStateList != null) {
            return new ColourStateListSpan(colorStateList);
        }
        int colour = resources.getColor(resourceId);
        if (colour >= 0) {
            return new StaticColourSpan(colour);
        }
        return null;
    }
}

If we change the StaticColourSpan and ColourStateListSpan classes to extend this base class rather than directly extent CharacterStyle we have a polymorphic newInstance() method which will return the appropriate object based upon the type of resource which gets loaded.

One final thing worth considering is how to determine the range of the string to which the Span should be applied. An obvious choice for string pattern matching is to use regular expressions, sop how about a utility class to do that for us:

public final class SpanUtils {
    private SpanUtils() {
    }

    public static CharSequence createSpannable(Context context, int stringId, Pattern pattern, CharacterStyle... styles) {
        String string = context.getString(stringId);
        return createSpannable(string, pattern, styles);
    }

    public static CharSequence createSpannable(CharSequence source, Pattern pattern, CharacterStyle... styles) {
        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(source);
        Matcher matcher = pattern.matcher(source);
        while (matcher.find()) {
            int start = matcher.start();
            int end = matcher.end();
            applyStylesToSpannable(spannableStringBuilder, start, end, styles);
        }
        return spannableStringBuilder;
    }

    private static SpannableStringBuilder applyStylesToSpannable(SpannableStringBuilder source, int start, int end, CharacterStyle... styles) {
        for (CharacterStyle style : styles) {
            source.setSpan(CharacterStyle.wrap(style), start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        }
        return source;
    }
}

We can now call this with the string, the regex pattern that we want to match, and a list of Span objects which should be applied wherever there is a match. This can easily be applied wherever it is needed, and this is arguably more concise than the Html implementation, but certainly much easier to both understand and maintain. And it works beautifully:

public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        CharacterStyle redText = TextColourSpan.newInstance(this, R.color.bright_red);
        CharacterStyle changeText = TextColourSpan.newInstance(this, R.color.pressable_string);
        Pattern redPattern = Pattern.compile(getString(R.string.simple_string_pattern));
        Pattern changePattern = Pattern.compile(getString(R.string.pressable_string_pattern));

        final TextView text2 = (TextView) findViewById(R.id.text2);
        final TextView text3 = (TextView) findViewById(R.id.text3);

        formatUsingSpans(text2, R.string.simple_string, redPattern, redText);

        formatUsingSpans(text3, R.string.pressable_string, redPattern, redText);
        formatUsingSpans(text3, changePattern, changeText);
    }

    private void formatUsingSpans(TextView textView, int stringId, Pattern pattern, CharacterStyle... styles) {
        CharSequence text = SpanUtils.createSpannable(this, stringId, pattern, styles);
        textView.setText(text);
    }
}

The accompanying source contains some further examples of how this technique can be used to use these few simple classes.

So, in conclusion: If I inherit any code that you’ve written and you have cut corners by using Html then I will hunt you down and seek my revenge! However, it is far more likely that you will have to maintain that code and you will end up hating yourself for it. Don’t create technical debt by using the Html class where it takes little more effort to do it the right way using Spans.

The source code for this article is available here.

© 2015, Mark Allison. All rights reserved.

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

2 Comments

  1. Nice article. Thanks for sharing.

    What kind of patterns would you use for a production code? (Keeping in mind that strings are going to be translated and translators need to be aware that something in certain strings should be kept together in order to highlight during the runtime)

    1. The trick is to load both your strings and regexs in resources. That way you can have different pattern matching for different locales.

Leave a Reply to GregoryK Cancel 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.