Android Spannablecontent With Rounded Corners
Asked Answered
M

9

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);
}
Militarize answered 11/7, 2014 at 15:52 Comment(9)
post an image what you wanna doJillianjillie
My reputation isn't high enough...Militarize
Well to add it to my original post. Something like this: i59.tinypic.com/16h7srr.pngMilitarize
ok, so now whats the problem with that blue round rect?Jillianjillie
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.Militarize
you already posted an answer, its here #19293338Jillianjillie
Yes, but that answer is written in C# (for Xamarin.Android) not Java. That answer won't compile.Militarize
come in, it is almost 1:1 relationship between those two languages, just see what methods he used and do the same in javaJillianjillie
Thanks, pskink. I think that I figured it out. In case anyone else is wondering you can use a converter like this - tangiblesoftwaresolutions.com/Product_Details/… to help you get started in the right direction.Militarize
M
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);
    }
}
Militarize answered 12/7, 2014 at 13:29 Comment(0)
K
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 :)

Kwan answered 5/1, 2016 at 23:53 Comment(4)
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.Kwan
This solution gave me a great starting point. I used extra line spacing to get around having to pass a font size in.Cleisthenes
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.Kwan
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.Cleisthenes
M
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);
    }
}
Militarize answered 12/7, 2014 at 13:29 Comment(0)
B
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);
  }
}
Bellwether answered 12/3, 2015 at 9:38 Comment(0)
M
9

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);
        }
    }
Midinette answered 4/4, 2015 at 2:20 Comment(0)
H
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

Historicism answered 28/7, 2017 at 14:40 Comment(3)
Nice. However, I notice when try to set padding top/bottom, then the rounded corner disappear. Do you know why?Supertax
@Supertax Not entirely sure how you're adding the padding. I recommend you creating a new question with the code and a screenshot.Historicism
how to give padding top and bottom , left , right inside background color ractangleAtlantean
O
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

Obstruct answered 2/10, 2018 at 3:3 Comment(2)
worked well! I needed to use this on a textview. I used this on a edit text and disabled the touch events.Chao
You can use val builder = SpannableStringBuilder() and then set it to textView.text = builderUpkeep
F
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
    }
Foskett answered 3/2, 2021 at 12:6 Comment(1)
how to give padding top and bottom , i have to give background color of spannable string now inside background color ractangle given padding top and bottom so that text should look good please suggestAtlantean
R
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);
}
Rheumatism answered 21/6, 2016 at 16:8 Comment(0)
S
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.

Scever answered 19/5, 2018 at 10:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.