Search code examples
androidcanvaspaintdrawtextspannable

How to draw a Spanned String with Canvas.drawText in Android


I want to draw a SpannedString to a Canvas.

enter image description here

SpannableString spannableString = new SpannableString("Hello World!");
ForegroundColorSpan foregroundSpan = new ForegroundColorSpan(Color.RED);
BackgroundColorSpan backgroundSpan = new BackgroundColorSpan(Color.YELLOW);
spannableString.setSpan(foregroundSpan, 1, 8, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannableString.setSpan(backgroundSpan, 3, spannableString.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);

The above example was drawn using a TextView, which in turn uses a Layout to draw text with spans. I know that using a Layout is the recommended way to draw text to the canvas. However, I am making my own text layout from scratch, so I need to implement this myself.

Doing something like this doesn't work

canvas.drawText(spannableString, 0, spannableString.length(), 0, 0, mTextPaint);

because drawText only gets the text from the spannableString, not any of the spans. The drawing colors are handled separately by TextPaint.

How do I use canvas.drawText (or drawTextRun) to draw the span information (specifically foreground and background color here)?

Related

Plan for a solution

I was going to directly do a self answer but this is turning out to be more difficult than I thought. So I will post first and then add an answer whenever I can figure it out. (I would of course welcome anyone to answer first.)

Here are the pieces that I have so far:


Solution

  • For most people coming to this question, you should probably use a StaticLayout to draw your spanned text. See this answer for help with that.

    However, if you actually do need to draw the spanned text yourself, then you will need to loop through all the spanned ranges and draw each one separately. You also need to measure the length of the text in each span so that you know where to start drawing the next span.

    The code below handles BackgroundColorSpan and ForegroundColorSpan.

    // set up the spanned string
    SpannableString spannableString = new SpannableString("Hello World!");
    ForegroundColorSpan foregroundSpan = new ForegroundColorSpan(Color.RED);
    BackgroundColorSpan backgroundSpan = new BackgroundColorSpan(Color.YELLOW);
    spannableString.setSpan(foregroundSpan, 1, 8, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    spannableString.setSpan(backgroundSpan, 3, spannableString.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    
    // draw each span one at a time
    int next;
    float xStart = 0;
    float xEnd;
    for (int i = 0; i < spannableString.length(); i = next) {
    
        // find the next span transition
        next = spannableString.nextSpanTransition(i, spannableString.length(), CharacterStyle.class);
    
        // measure the length of the span
        xEnd = xStart + mTextPaint.measureText(spannableString, i, next);
    
        // draw the highlight (background color) first
        BackgroundColorSpan[] bgSpans = spannableString.getSpans(i, next, BackgroundColorSpan.class);
        if (bgSpans.length > 0) {
            mHighlightPaint.setColor(bgSpans[0].getBackgroundColor());
            canvas.drawRect(xStart, mTextPaint.getFontMetrics().top, xEnd, mTextPaint.getFontMetrics().bottom, mHighlightPaint);
        }
    
        // draw the text with an optional foreground color
        ForegroundColorSpan[] fgSpans = spannableString.getSpans(i, next, ForegroundColorSpan.class);
        if (fgSpans.length > 0) {
            int saveColor = mTextPaint.getColor();
            mTextPaint.setColor(fgSpans[0].getForegroundColor());
            canvas.drawText(spannableString, i, next, xStart, 0, mTextPaint);
            mTextPaint.setColor(saveColor);
        } else {
            canvas.drawText(spannableString, i, next, xStart, 0, mTextPaint);
        }
    
        xStart = xEnd;
    }
    

    The top string in the image below was drawn with the code above. The bottom string was drawn with a regular TextView (which uses a StaticLayout).

    enter image description here