Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android - Add Margin for SpannableStringBuilder using ReplacementSpan

I'm trying to format Hashtags inside a TextView/EditText (Say like Chips mentioned in the Material Design Specs). I'm able to format the background using ReplacementSpan. But the problem is that I'm not able to increase the line spacing in the TextView/EditText. See the image below enter image description here

The question is how do I add top and bottom margin for the hashtags?

Here is the code where I add the background to the text:

   /**
     * First draw a rectangle
     * Then draw text on top
     */
    @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 + measureText(paint, text, start, end), bottom);
        paint.setColor(backgroundColor);
        canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, paint);
        paint.setColor(textColor);
        canvas.drawText(text, start, end, x, y, paint);
    }
like image 563
PsyGik Avatar asked Aug 09 '16 12:08

PsyGik


People also ask

What is SpannableString?

android.text.SpannableString. This is the class for text whose content is immutable but to which markup objects can be attached and detached. For mutable text, see SpannableStringBuilder .

What is Spanned text in Android?

Spans are powerful markup objects that you can use to style text at a character or paragraph level. By attaching spans to text objects, you can change text in a variety of ways, including adding color, making the text clickable, scaling the text size, and drawing text in a customized way.

How to set color in Spannable String in Android?

For example, to set a green text color you would create a SpannableString based on the text and set the span. SpannableString string = new SpannableString("Text with a foreground color span"); string. setSpan(new ForegroundColorSpan(color), 12, 28, Spanned.


2 Answers

I had a similar problem a while ago and this is the solution I've come up with:

The hosting TextView in xml:

<TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="18dp"
        android:paddingBottom="18dp"
        android:paddingLeft="8dp"
        android:paddingRight="8dp"
        android:gravity="fill"
        android:textSize="12sp"
        android:lineSpacingExtra="10sp"
        android:textStyle="bold"
        android:text="@{viewModel.renderedTagBadges}">

A custom version of ReplacementSpan

public class TagBadgeSpannable extends ReplacementSpan implements LineHeightSpan {

    private static int CORNER_RADIUS = 30;
    private final int textColor;
    private final int backgroundColor;
    private final int lineHeight;

    public TagBadgeSpannable(int lineHeight, int textColor, int backgroundColor) {
        super();
        this.textColor = textColor;
        this.backgroundColor = backgroundColor;
        this.lineHeight = lineHeight;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        final float textSize = paint.getTextSize();
        final float textLength = x + measureText(paint, text, start, end);
        final float badgeHeight = textSize * 2.25f;
        final float textOffsetVertical = textSize * 1.45f;

        RectF badge = new RectF(x, y, textLength, y + badgeHeight);
        paint.setColor(backgroundColor);
        canvas.drawRoundRect(badge, CORNER_RADIUS, CORNER_RADIUS, paint);

        paint.setColor(textColor);
        canvas.drawText(text, start, end, x, y + textOffsetVertical, paint);
    }

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

    private float measureText(Paint paint, CharSequence text, int start, int end) {
        return paint.measureText(text, start, end);
    }

    @Override
    public void chooseHeight(CharSequence charSequence, int i, int i1, int i2, int i3, Paint.FontMetricsInt fontMetricsInt) {
        fontMetricsInt.bottom += lineHeight;
        fontMetricsInt.descent += lineHeight;
    }
}

And finally a builder that creates the Spannable

public class AndroidTagBadgeBuilder implements TagBadgeBuilder {

    private final SpannableStringBuilder stringBuilder;
    private final String textColor;
    private final int lineHeight;

    public AndroidTagBadgeBuilder(SpannableStringBuilder stringBuilder, int lineHeight, String textColor) {
        this.stringBuilder = stringBuilder;
        this.lineHeight = lineHeight;
        this.textColor = textColor;
    }

    @Override
    public void appendTag(String tagName, String badgeColor) {
        final String nbspSpacing = "\u202F\u202F"; // none-breaking spaces

        String badgeText = nbspSpacing + tagName + nbspSpacing;
        stringBuilder.append(badgeText);
        stringBuilder.setSpan(
            new TagBadgeSpannable(lineHeight, Color.parseColor(textColor), Color.parseColor(badgeColor)),
            stringBuilder.length() - badgeText.length(),
            stringBuilder.length()- badgeText.length() + badgeText.length(),
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        );
        stringBuilder.append("  ");
    }

    @Override
    public CharSequence getTags() {
        return stringBuilder;
    }

    @Override
    public void clear() {
        stringBuilder.clear();
        stringBuilder.clearSpans();
    }
}

The outcome will look something like this: Rendered badges in TextView

Tweak the measures in TagBadgeSpannable to your liking.

I've uploaded a very minimal sample project using this code to github so feel free to check it out.

NOTE: The sample uses Android Databinding and is written MVVM style

like image 65
Daniel W. Avatar answered Sep 21 '22 11:09

Daniel W.


Text markup in Android is so poorly documented, writing this code is like feeling your way through the dark.

I've done a little bit of it, so I will share what I know.

You can handle line spacing by wrapping your chip spans inside a LineHeightSpan. LineHeightSpan is an interface that extends the ParagraphStyle marker interface, so this tells you it affects appearance at a paragraph level. Maybe a good way to explain it is to compare your ReplacementSpan subclass to an HTML <span>, whereas a ParagraphStyle span like LineHeightSpan is like an HTML <div>.

The LineHeightSpan interface consists of one method:

public void chooseHeight(CharSequence text, int start, int end,
                         int spanstartv, int v,
                         Paint.FontMetricsInt fm);

This method is called for each line in your paragraph

  • text is your Spanned string.
  • start is the index of the character at the start of the current line
  • end is the index of the character at the end of the current line
  • spanstartv is (IIRC) the vertical offset of the entire span itself
  • v is (IIRC) the vertical offset of the current line
  • fm is the FontMetrics object, which is actually a returned (in/out) parameter. Your code will make changes to fm and TextView will use those when drawing.

So what the TextView will do is call this method once for every line it processes. Based on the parameters, along with your Spanned string, you set up the FontMetrics to render the line with the values of your choosing.

Here's an example I did for a bullet item in a list (think <ol><li>) where I wanted some separation between each list item:

    @Override
    public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm) {

        int incr = Math.round(.36F * fm.ascent); // note: ascent is negative

        // first line: add space to the top
        if (((Spanned) text).getSpanStart(this) == start) {
            fm.ascent += incr;
            fm.top = fm.ascent + 1;
        }

        // last line: add space to the bottom
        if (((Spanned) text).getSpanEnd(this) == end) {
            fm.bottom -= incr;
        }

    }

Your version will probably be even simpler, just changing the FontMetrics the same way for each line that it's called.

When it comes to deciphering the FontMetrics, the logger and debugger are your friends. You'll just have to keep tweaking values until you get something you like.

like image 26
kris larson Avatar answered Sep 22 '22 11:09

kris larson