Search code examples
androidhashtagspannablestringandroid-chips

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);
    }

Solution

  • 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