12

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:

Community
  • 1
  • 1
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
  • so what is the problem with `Layout#draw` actually? – pskink Apr 07 '17 at 10:10
  • @pskink, the standard `Layout` only handles LTR and RTL horizontal text. I am making a vertical text layout for [traditional Mongolian](http://stackoverflow.com/q/29739720/3681880). I've tried various [Layout hacks](http://stackoverflow.com/a/29739721/3681880) in the past, but even those don't allow me to rotate emoji or CJK characters. So I am starting from scratch. [I have the basic layout functionality finished](https://github.com/suragch/mongol-library/blob/master/mongol-library/src/main/java/net/studymongolian/mongollibrary/MongolLayout.java); now I need to support spanned text. – Suragch Apr 07 '17 at 10:27
  • ok, now i see it is not a "normal" `Hello World` you presented in your picture – pskink Apr 07 '17 at 10:35
  • so you want something like [this](http://pasteboard.co/8GVw7Cju.png)? – pskink Apr 07 '17 at 11:40
  • @pskink, that is correct. Although, for the purpose of this question, there is no need to worry about the vertical rotation or left to right line wrapping. – Suragch Apr 07 '17 at 12:57
  • What you're asking is either vague or broad(..._"because `drawText` wants a `String`, not a `SpannableString`"_. It's because you're using too low level of the inheritance hierarchy, `Canvas.drawText()` takes `CharSequence` also, so it as polymorphic type for the `spannableString` variable. I used same approach in the sample to [this answer](http://stackoverflow.com/questions/31837840/paginating-text-in-android/32096884#32096884)... _"TextPaint should be setting things like the colors"_ It's not clear what the problem is. – Onik Apr 07 '17 at 20:02
  • 1
    @Onik, You're right. I was using the wrong `drawText` method. I updated my question. The problem is that `drawText` still doesn't use the span information in the `CharSequence`. – Suragch Apr 14 '17 at 06:49

1 Answers1

9

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

Community
  • 1
  • 1
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
  • You can handle most other spans the same way the foreground span is handled. In fact there is an update draw state method that will do it automatically. (I am just adding this as a comment for now because I can't remember the details off hand. If this is something you would like more info about then ask me about it and I will update this answer.) – Suragch Jun 03 '17 at 01:51