Android Spannablecontent With Rounded Corners

After reading getting a little help with a converter for C#, I came up with this. I still have some tweaking to do, but if anyone is also looking for a similar answer.

public class RoundedBackgroundSpan extends ReplacementSpan
{

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        return 0;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint)
    {
        RectF rect = new RectF(x, top, x + text.length(), bottom);
        paint.setColor(Color.CYAN);
        canvas.drawRoundRect(rect, 20, 20, paint);
        paint.setColor(Color.WHITE);
        canvas.drawText(text, start, end, x, y, paint);
    }
}

Here's an improved version based on @ericlokness answer, with custom background and text colors. It also works with multiple spans on the same TextView.

public class RoundedBackgroundSpan extends ReplacementSpan
{
  private final int _padding = 20;
  private int _backgroundColor;
  private int _textColor;

  public RoundedBackgroundSpan(int backgroundColor, int textColor) {
    super();
    _backgroundColor = backgroundColor;
    _textColor = textColor;
  }

  @Override
  public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
    return (int) (_padding + paint.measureText(text.subSequence(start, end).toString()) + _padding);
  }

  @Override
  public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint)
  {
    float width = paint.measureText(text.subSequence(start, end).toString());
    RectF rect = new RectF(x - _padding, top, x + width + _padding, bottom);
    paint.setColor(_backgroundColor);
    canvas.drawRoundRect(rect, 20, 20, paint);
    paint.setColor(_textColor);
    canvas.drawText(text, start, end, x, y, paint);
  }
}

Actually i found big issues with all of those answers when displaying multiple lines of badges. After lots of testing and tweaking. I Finally got the best version of the above.

The basic idea is to trick the TextView by setting a much bigger text size and setting the wanted size inside the span. Also, you can see i'm drawing the badge background and text differently.

So, this is my RoundedBackgroundSpan:

public class RoundedBackgroundSpan extends ReplacementSpan {

    private static final int CORNER_RADIUS = 12;

    private static final float PADDING_X = GeneralUtils.convertDpToPx(12);
    private static final float PADDING_Y = GeneralUtils.convertDpToPx(2);

    private static final float MAGIC_NUMBER = GeneralUtils.convertDpToPx(2);

    private int mBackgroundColor;
    private int mTextColor;
    private float mTextSize;

    /**
     * @param backgroundColor color value, not res id
     * @param textSize        in pixels
     */
    public RoundedBackgroundSpan(int backgroundColor, int textColor, float textSize) {
        mBackgroundColor = backgroundColor;
        mTextColor = textColor;
        mTextSize = textSize;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        paint = new Paint(paint); // make a copy for not editing the referenced paint

        paint.setTextSize(mTextSize);

        // Draw the rounded background
        paint.setColor(mBackgroundColor);
        float textHeightWrapping = GeneralUtils.convertDpToPx(4);
        float tagBottom = top + textHeightWrapping + PADDING_Y + mTextSize + PADDING_Y + textHeightWrapping;
        float tagRight = x + getTagWidth(text, start, end, paint);
        RectF rect = new RectF(x, top, tagRight, tagBottom);
        canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, paint);

        // Draw the text
        paint.setColor(mTextColor);
        canvas.drawText(text, start, end, x + PADDING_X, tagBottom - PADDING_Y - textHeightWrapping - MAGIC_NUMBER, paint);
    }

    private int getTagWidth(CharSequence text, int start, int end, Paint paint) {
        return Math.round(PADDING_X + paint.measureText(text.subSequence(start, end).toString()) + PADDING_X);
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        paint = new Paint(paint); // make a copy for not editing the referenced paint
        paint.setTextSize(mTextSize);
        return getTagWidth(text, start, end, paint);
    }
}

And here is how i'm using it:

public void setTags(ArrayList<String> tags) {
    if (tags == null) {
        return;
    }

    mTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 26); // Tricking the text view for getting a bigger line height
    
    SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
    
    String between = " ";
    int tagStart = 0;
    
    float textSize = 13 * getResources().getDisplayMetrics().scaledDensity; // sp to px
    
    for (String tag : tags) {
        // Append tag and space after
        stringBuilder.append(tag);
        stringBuilder.append(between);

        // Set span for tag
        RoundedBackgroundSpan tagSpan = new RoundedBackgroundSpan(bgColor, textColor, textSize);
        stringBuilder.setSpan(tagSpan, tagStart, tagStart + tag.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

        // Update to next tag start
        tagStart += tag.length() + between.length();
    }

    mTextView.setText(stringBuilder, TextView.BufferType.SPANNABLE);
}

**Note:**
  • You can play with all sizes and constants to fit to your wanted style
  • If you use an external font be sure to set android:includeFontPadding="false" otherwise it can mess up the line's height

Enjoy :)