23

I am trying to change my string to make a badge with a number in the middle by using Spannable String. I can highlight the appropriate letter/number by setting the BackGroundColorSpan, but need help making it a little prettier. I was hoping to have rounded corners with a little bit of padding around the entire shape.

This article is really close to what I'm trying to do: Android SpannableString set background behind part of text

I really need to keep the resource as a TextView due to the way it interacts with my application.

Any ideas how to utilize ReplacementSpan for my particular situation?

Here is my code snippet:

if (menuItem.getMenuItemType() == SlidingMenuItem.MenuItemType.NOTIFICATIONS) {
    myMenuRow.setTypeface(null, Typeface.NORMAL);
    myMenuRow.setTextColor(getContext().getResources().getColor(R.color.BLACK));
    myMenuRow.setActivated(false);
    SpannableString spannablecontent = new SpannableString(myMenuRow.getText());
    spannablecontent.setSpan(new BackgroundColorSpan(Color.argb(150,0,0,0)), 18, myMenuRow.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    myMenuRow.setText(spannablecontent);
}
stkent
  • 19,772
  • 14
  • 85
  • 111
ericlokness
  • 505
  • 1
  • 4
  • 12
  • post an image what you wanna do – pskink Jul 11 '14 at 16:08
  • My reputation isn't high enough... – ericlokness Jul 11 '14 at 16:19
  • Well to add it to my original post. Something like this: http://i59.tinypic.com/16h7srr.png – ericlokness Jul 11 '14 at 16:23
  • ok, so now whats the problem with that blue round rect? – pskink Jul 11 '14 at 16:32
  • You asked what I want to do. That is what I WANT it to look like. Right now it's just a square BackgroundColorSpan item that has no rounded corners around my item. – ericlokness Jul 12 '14 at 06:43
  • you already posted an answer, its here http://stackoverflow.com/questions/19292838/android-spannablestring-set-background-behind-part-of-text – pskink Jul 12 '14 at 07:35
  • Yes, but that answer is written in C# (for Xamarin.Android) not Java. That answer won't compile. – ericlokness Jul 12 '14 at 09:41
  • come in, it is almost 1:1 relationship between those two languages, just see what methods he used and do the same in java – pskink Jul 12 '14 at 09:48
  • Thanks, pskink. I think that I figured it out. In case anyone else is wondering you can use a converter like this - http://www.tangiblesoftwaresolutions.com/Product_Details/CSharp_to_Java_Converter_Details.html to help you get started in the right direction. – ericlokness Jul 12 '14 at 13:22

9 Answers9

25

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 :)

Top-Master
  • 7,611
  • 5
  • 39
  • 71
Shirane85
  • 2,255
  • 1
  • 26
  • 38
  • 1
    I've added MAGIC_NUMBER because from some reason i couldn't get the text to be vertical centered and i don't have a clue why but it was drawn 2 dp below. This was tested on several resolutions. – Shirane85 Apr 22 '16 at 13:12
  • This solution gave me a great starting point. I used extra line spacing to get around having to pass a font size in. – Richard Le Mesurier Apr 24 '16 at 14:42
  • From what i remember, extra line spacing made the height of the first or last rows different if the are more than 2 lines. That's why i did it this way. – Shirane85 Apr 24 '16 at 14:51
  • Thx for the heads-up - Just confirmed looks fine on my side with any number of lines. But you are right, the extra spacing is only between lines, not below the last line, so I ended up drawing outside the `TextView` bounds. To get around that I added padding to the bottom of the view - one hack or the other either way. – Richard Le Mesurier Apr 25 '16 at 05:18
  • 1
    Great, happy my answer helped you :) – Shirane85 Apr 25 '16 at 05:27
12

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);
    }
}
ericlokness
  • 505
  • 1
  • 4
  • 12
12

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);
  }
}
mvandillen
  • 904
  • 10
  • 17
8

I further improved mvandillen class.

This seems to work very fine:

public class RoundedBackgroundSpan extends ReplacementSpan
    {
        private final int mPadding = 10;
        private int mBackgroundColor;
        private int mTextColor;

        public RoundedBackgroundSpan(int backgroundColor, int textColor) {
            super();
            mBackgroundColor = backgroundColor;
            mTextColor = textColor;
        }

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

        @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, top+mPadding, x + width + 2*mPadding, bottom);
            paint.setColor(mBackgroundColor);
            canvas.drawRoundRect(rect, mPadding, mPadding, paint);
            paint.setColor(mTextColor);
            canvas.drawText(text, start, end, x+mPadding, y, paint);
        }
    }
Daniele B
  • 19,801
  • 29
  • 115
  • 173
7

Here is my version based on @mvandillen answer. I also needed some margin at the beginning of span.

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.NonNull;
import android.text.style.ReplacementSpan;

public class CoolBackgroundColorSpan extends ReplacementSpan {

    private final int mBackgroundColor;
    private final int mTextColor;
    private final float mCornerRadius;
    private final float mPaddingStart;
    private final float mPaddingEnd;
    private final float mMarginStart;

    public CoolBackgroundColorSpan(int backgroundColor, int textColor, float cornerRadius, float paddingStart, float paddingEnd, float marginStart) {
        super();
        mBackgroundColor = backgroundColor;
        mTextColor = textColor;
        mCornerRadius = cornerRadius;
        mPaddingStart = paddingStart;
        mPaddingEnd = paddingEnd;
        mMarginStart = marginStart;
    }

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

    @Override
    public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
        float width = paint.measureText(text.subSequence(start, end).toString());
        RectF rect = new RectF(x - mPaddingStart + mMarginStart, top, x + width + mPaddingEnd + mMarginStart, bottom);
        paint.setColor(mBackgroundColor);
        canvas.drawRoundRect(rect, mCornerRadius, mCornerRadius, paint);
        paint.setColor(mTextColor);
        canvas.drawText(text, start, end, x + mMarginStart, y, paint);
    }
}

How to use:

int flag = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
SpannableString staffTitleSpan = new SpannableString("staff: ");
SpannableString staffNameSpan = new SpannableString("John Smith");
staffNameSpan.setSpan(new StyleSpan(Typeface.BOLD), 0, staffNameSpan.length(), flag);
staffNameSpan.setSpan(new CoolBackgroundColorSpan(mStaffNameSpanBgColor, mStaffNameSpanTextColor, mStaffNameSpanBgRadius, mStaffNameSpanBgPaddingStart, mStaffNameSpanBgPaddingEnd, mStaffNameSpanMarginStart), 0, staffNameSpan.length(), flag);
SpannableStringBuilder builder = new SpannableStringBuilder();
builder.append(staffTitleSpan);
builder.append(staffNameSpan);

staffTextView.setText(builder);

Preview:

enter image description here

vovahost
  • 34,185
  • 17
  • 113
  • 116
  • Nice. However, I notice when try to set padding top/bottom, then the rounded corner disappear. Do you know why? – anticafe Nov 05 '20 at 17:32
  • @anticafe Not entirely sure how you're adding the padding. I recommend you creating a new question with the code and a screenshot. – vovahost Nov 06 '20 at 16:27
7

Hopefully this answer simplifies it for those still looking...

You can simply use a "chip" drawable. It does all the calculations correctly and is of much more minimal code.

See Standalone ChipDrawable

For completeness copied here:

res/xml/standalone_chip.xml:

<chip xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:text="@string/item_browse_database_sample"
    app:chipBackgroundColor="@color/blueBase"
    app:closeIconVisible="false" />

in java:

// Inflate from resources.
ChipDrawable chip = ChipDrawable.createFromResource(getContext(), R.xml.standalone_chip);

// Use it as a Drawable however you want.
chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight());
ImageSpan span = new ImageSpan(chip);

Editable text = editText.getText();
text.setSpan(span, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

End result:

result

JRomero
  • 4,878
  • 1
  • 27
  • 49
4

If you are using kotlin and targeting multiple density devices then this would work for you

Step 1 : Create a class i.e RoundedBackgroundSpan.kt that extend ReplacementSpan

import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.text.style.ReplacementSpan
import kotlin.math.roundToInt

class RoundedBackgroundSpan(
    private val textColor: Int,
    private val backgroundColor: Int
) : ReplacementSpan() {

    private val additionalPadding = 4.toPx().toFloat()
    private val cornerRadius = 4.toPx().toFloat()

    override fun draw(
        canvas: Canvas,
        text: CharSequence,
        start: Int,
        end: Int,
        x: Float,
        top: Int,
        y: Int,
        bottom: Int,
        paint: Paint
    ) {
        val newTop = y + paint.fontMetrics.ascent
        val newBottom = y + paint.fontMetrics.descent
        val rect = RectF(x, newTop, x + measureText(paint, text, start, end) + 2 * additionalPadding, newBottom)
        paint.color = backgroundColor

        canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint)
        paint.color = textColor
        canvas.drawText(text, start, end, x + additionalPadding, y.toFloat(), paint)
    }

    override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
        return (paint.measureText(text, start, end) + 2 * additionalPadding).toInt()
    }

    private fun measureText(paint: Paint, text: CharSequence, start: Int, end: Int): Float {
        return paint.measureText(text, start, end)
    }

    private fun Int.toPx(): Int {
        val resources = Resources.getSystem()
        val metrics = resources.displayMetrics
        return (this * (metrics.densityDpi / 160.0f)).roundToInt()
    }
}

Step 2 : After that call the above created class like below

   private fun updateSubjectName(textView: TextView, fullText: String, spanText: String, spanColor: String) {
        val flag = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        val fullTextSpan = SpannableString("$fullText ")
        val spanTextSpan = SpannableString("$spanText")
        spanTextSpan.setSpan(StyleSpan(Typeface.BOLD), 0, spanTextSpan.length, flag)

        spanTextSpan.setSpan(
            RoundedBackgroundSpan(context.getColor(R.color.color_white), Color.parseColor(spanColor)),
            0, spanTextSpan.length, flag
        )

        val builder = SpannableStringBuilder()
        builder.append(fullTextSpan)
        builder.append(spanTextSpan)
        textView.text = builder
    }
Suraj Bahadur
  • 3,730
  • 3
  • 27
  • 55
3

Ok, so the question is a bit messy, here is my solution from DanieleB and mvandillen.

public class RoundedBackgroundSpan extends ReplacementSpan {

    private static final int CORNER_RADIUS = 8;
    private static final int PADDING_X = 12;

    private int   mBackgroundColor;
    private int   mTextColor;

    /**
     * @param backgroundColor background color
     * @param textColor       text color
     */
    public RoundedBackgroundSpan(int backgroundColor, int textColor) {
        mBackgroundColor = backgroundColor;
        mTextColor = textColor;
    }

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

    @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, top, x + width + 2 * PADDING_X, bottom);
        paint.setColor(mBackgroundColor);
        canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, paint);
        paint.setColor(mTextColor);
        canvas.drawText(text, start, end, x + PADDING_X, y, paint);
    }
}

Tip: you can remove the textColor and user the default TextView color:

@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
    Paint paint1 = new Paint(paint);
    float width = paint1.measureText(text.subSequence(start, end).toString());
    RectF rect = new RectF(x, top, x + width + 2 * PADDING_X, bottom);
    paint1.setColor(mBackgroundColor);
    canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, paint1);
    canvas.drawText(text, start, end, x + PADDING_X, y, paint);
}
Hugo Gresse
  • 17,195
  • 9
  • 77
  • 119
0

Watching Google's video, they offer this solution:

enter image description here enter image description here enter image description here enter image description here enter image description here enter image description here

Sadly I see here various things missing, and I can't find the full code, so I can't try it out.

android developer
  • 114,585
  • 152
  • 739
  • 1,270