How to adjust text kerning in Android TextView?
Asked Answered
B

12

30

Is there a way to adjust the spacing between characters in an Android TextView? I believe this is typically called "kerning".

I'm aware of the android:textScaleX attribute, but that compresses the characters along with the spacing.

Barbee answered 28/10, 2009 at 22:54 Comment(0)
E
12

AFAIK, you cannot adjust kerning in TextView. You may be able to adjust kerning if you draw the text on the Canvas yourself using the 2D graphics APIs.

Essence answered 29/10, 2009 at 14:18 Comment(5)
well, I guess that's that then. Cheers, MikeBarbee
Hi, @CommonsWare, How do I adjust kerning if I draw text on the Canvas using the 2D graphics APIs? Would you give me a clue?Pappas
You can implement a kerning adjustment by supplying your own modified version of the font used.Diu
@Essence Since API21, 'letterSpacing' does the same. But what about backward-compatibility? I was looking into styles of library v7, but seems there is not support for it. Can you please give us your finding on this?Bidarka
No longer true: please check out android:letterSpacing developer.android.com/reference/android/widget/…Stannum
K
71

I built a custom class that extends TextView and adds a method "setSpacing". The workaround is similar to what @Noah said. The method adds a space between each letter of the String and with SpannedString changes the TextScaleX of the spaces, allowing positive and negative spacing.

/**
 * Text view that allows changing the letter spacing of the text.
 * 
 * @author Pedro Barros (pedrobarros.dev at gmail.com)
 * @since May 7, 2013
 */

import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ScaleXSpan;
import android.util.AttributeSet;
import android.widget.TextView;

public class LetterSpacingTextView extends TextView {

    private float spacing = Spacing.NORMAL;
    private CharSequence originalText = "";


    public LetterSpacingTextView(Context context) {
        super(context);
    }

    public LetterSpacingTextView(Context context, AttributeSet attrs){
        super(context, attrs);
    }

    public LetterSpacingTextView(Context context, AttributeSet attrs, int defStyle){
        super(context, attrs, defStyle);
    }

    public float getSpacing() {
        return this.spacing;
    }

    public void setSpacing(float spacing) {
        this.spacing = spacing;
        applySpacing();
    }

    @Override
    public void setText(CharSequence text, BufferType type) {
        originalText = text;
        applySpacing();
    }

    @Override
    public CharSequence getText() {
        return originalText;
    }

    private void applySpacing() {
        if (this == null || this.originalText == null) return;
        StringBuilder builder = new StringBuilder();
        for(int i = 0; i < originalText.length(); i++) {
            builder.append(originalText.charAt(i));
            if(i+1 < originalText.length()) {
                builder.append("\u00A0");
            }
        }
        SpannableString finalText = new SpannableString(builder.toString());
        if(builder.toString().length() > 1) {
            for(int i = 1; i < builder.toString().length(); i+=2) {
                finalText.setSpan(new ScaleXSpan((spacing+1)/10), i, i+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
        super.setText(finalText, BufferType.SPANNABLE);
    }

    public class Spacing {
        public final static float NORMAL = 0;
    }
}

Using it:

LetterSpacingTextView textView = new LetterSpacingTextView(context);
textView.setSpacing(10); //Or any float. To reset to normal, use 0 or LetterSpacingTextView.Spacing.NORMAL
textView.setText("My text");
//Add the textView in a layout, for instance:
((LinearLayout) findViewById(R.id.myLinearLayout)).addView(textView);
Kid answered 7/5, 2013 at 22:40 Comment(10)
This code works fine for reducing the letter spacing but unfortunately it breaks the text wrapping for me.Monitor
Replacing builder.append(" ") with no-break space builder.append("\u00A0") solved the issue. Thanks for the code!Monitor
thank you. i tried but it only works by setText()-ing. i already have text attribute set and tried to applyLetterSpacing() on parseAttrs() but get blank view, instead. what should i do?Coopersmith
By the way, you can use SpannedStringBuilder and insert the spaces plus set the spans in the same loop!Elegit
Thanks @Sam! Finally Google implemented this! Edited ;)Kid
Is it possible to add the spacing value in the styles xml file or layout xml file? Thanks.Hypnotist
Just a heads up if anyone is trying to use this. If you have android:textAllCaps set to true, it will remove you widths on the spans and cause it not to render correctlyClairvoyance
Thanks @TrevorSStone, I couldn't figure out why this wasn't working on my Button view I made (dang Button view has textAllCaps set to true by default!)Cenacle
@PedroBarros I found an issue in this. when text increase, text not goes to next line.Quip
@ErKimmiDhingra try switching "\u00A0" to " ", since the former is a no-break space... If this is the problem, it might work.Kid
E
14

If anyone is looking for a simple way to apply the kerning to any string (technically, CharSequence) without using a TextView:

public static Spannable applyKerning(CharSequence src, float kerning)
{
    if (src == null) return null;
    final int srcLength = src.length();
    if (srcLength < 2) return src instanceof Spannable
                              ? (Spannable)src
                              : new SpannableString(src);

    final String nonBreakingSpace = "\u00A0";
    final SpannableStringBuilder builder = src instanceof SpannableStringBuilder
                                           ? (SpannableStringBuilder)src
                                           : new SpannableStringBuilder(src);
    for (int i = src.length() - 1; i >= 1; i--)
    {
        builder.insert(i, nonBreakingSpace);
        builder.setSpan(new ScaleXSpan(kerning), i, i + 1,
                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

    return builder;
}
Elegit answered 29/11, 2013 at 19:59 Comment(0)
E
12

AFAIK, you cannot adjust kerning in TextView. You may be able to adjust kerning if you draw the text on the Canvas yourself using the 2D graphics APIs.

Essence answered 29/10, 2009 at 14:18 Comment(5)
well, I guess that's that then. Cheers, MikeBarbee
Hi, @CommonsWare, How do I adjust kerning if I draw text on the Canvas using the 2D graphics APIs? Would you give me a clue?Pappas
You can implement a kerning adjustment by supplying your own modified version of the font used.Diu
@Essence Since API21, 'letterSpacing' does the same. But what about backward-compatibility? I was looking into styles of library v7, but seems there is not support for it. Can you please give us your finding on this?Bidarka
No longer true: please check out android:letterSpacing developer.android.com/reference/android/widget/…Stannum
A
7

Since Android 21, you can use set the letterSpacing attribute.

<TextView
    android:width="..."
    android:height="..."
    android:letterSpacing="1.3"/>
Appointee answered 18/6, 2019 at 7:53 Comment(0)
K
4

Here's my solution, which adds uniform spacing (in pixels) between each character. This span assumes all text is in a single line. This basically implements what @commonsWare suggests.

SpannableStringBuilder builder = new SpannableStringBuilder("WIDE normal");
builder.setSpan(new TrackingSpan(20), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
...

private static class TrackingSpan extends ReplacementSpan {
    private float mTrackingPx;

    public TrackingSpan(float tracking) {
        mTrackingPx = tracking;
    }

    @Override
    public int getSize(Paint paint, CharSequence text, 
        int start, int end, Paint.FontMetricsInt fm) {
        return (int) (paint.measureText(text, start, end) 
            + mTrackingPx * (end - start - 1));
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, 
        int start, int end, float x, int top, int y, 
        int bottom, Paint paint) {
        float dx = x;
        for (int i = start; i < end; i++) {
            canvas.drawText(text, i, i + 1, dx, y, paint);
            dx += paint.measureText(text, i, i + 1) + mTrackingPx;
        }
    }
}
Kyl answered 15/7, 2014 at 23:29 Comment(0)
D
3

The only way I found to adjust the kerning, is to create a custom font in which the glyph advance is altered.

Diu answered 10/1, 2013 at 11:1 Comment(0)
V
2

It's difficult to adjust spacing between characters, when you are using TextView. But if you can handle the drawing yourself, there should be some way to do that.

My answer to this question is: use your custom Span .

My code:

public class LetterSpacingSpan extends ReplacementSpan {
    private int letterSpacing = 0;

    public LetterSpacingSpan spacing(int space) {
        letterSpacing = space;

        return this;
    }


    @Override
    public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) {
        return (int) paint.measureText(text, start, end) + (text.length() - 1) * letterSpacing;
    }


    @Override
    public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
        int length = text.length();
        float currentX = x;

        for (int i = 1; i < length; i++) {          
            canvas.drawText(text, i, i + 1, currentX, y, paint);
            currentX += paint.measureText(text, i, i + 1) + letterSpacing;
         }
    }
}

Explain:

Building your own Span can help you achieve many amazing effect, like make a blur TextView, change the background or foreground for your TextView, even make some animation. I learn a lot from this post Span a powerful concept .

Because you are adding spacing to each character, so we should use a character level base span, in this case, ReplacementSpan is the best choice. I add a spacing method, so when using it, you can simply pass the space you want for each character as parameter.

When building your custom span, you need to override at least two method, getSize and draw. The getSize method should return the final width after we add the spacing for the whole charsequence, and inside the draw method block, you can control the Canvas to do the drawing you want.

So how we use this LetterSpacingSpan? It's easy:

Usage:

TextView textView;
Spannable content = new SpannableString("This is the content");
textView.setSpan(new LetterSpacingSpan(), 0, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(content);

And that's it.

Versicular answered 14/6, 2018 at 8:22 Comment(0)
C
1

You can also try using a SpannedString but you would need to parse it and change the character spacing for each of the words

Cervantes answered 25/1, 2011 at 23:27 Comment(0)
S
1

This answer may be helpful for someone who wants to draw text with kerning on a Canvas, using drawText (this is not about text in a TextView).

Since Lollipop, the method setLetterSpacing is available on Paint. If the SDK is LOLLIPOP and on, setLetterSpacing is used. Otherwise, a method is invoked that does something similar to @dgmltn's suggestion above:

    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
        paint.setLetterSpacing(-0.04f);  // setLetterSpacing is only available from LOLLIPOP and on
        canvas.drawText(text, xOffset, yOffset, paint);
    } else {
        float spacePercentage = 0.05f;
        drawKernedText(canvas, text, xOffset, yOffset, paint, spacePercentage);
    }


/**
 * Programatically drawn kerned text by drawing the text string character by character with a space in between.
 * Return the width of the text.
 * If canvas is null, the text won't be drawn, but the width will still be returned
 * kernPercentage determines the space between each letter. If it's 0, there will be no space between letters.
 * Otherwise, there will be space between each letter. The  value is a fraction of the width of a blank space.
 */
private int drawKernedText(Canvas canvas, String text, float xOffset, float yOffset, Paint paint, float kernPercentage) {
    Rect textRect = new Rect();
    int width = 0;
    int space = Math.round(paint.measureText(" ") * kernPercentage);
    for (int i = 0; i < text.length(); i++) {
        if (canvas != null) {
            canvas.drawText(String.valueOf(text.charAt(i)), xOffset, yOffset, paint);
        }
        int charWidth;
        if (text.charAt(i) == ' ') {
            charWidth = Math.round(paint.measureText(String.valueOf(text.charAt(i)))) + space;
        } else {
            paint.getTextBounds(text, i, i + 1, textRect);
            charWidth = textRect.width() + space;
        }
        xOffset += charWidth;
        width += charWidth;
    }
    return width;
}
Sym answered 27/3, 2016 at 6:21 Comment(0)
M
0

There's a small edit of @Pedro Barros answer. It is useful if you use SpannableString to set it, e.g. if you want to make different colors of some characters:

private void applySpacing() {
    SpannableString finalText;

    if (!(originalText instanceof SpannableString)) {
        if (this.originalText == null) return;
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < originalText.length(); i++) {
            builder.append(originalText.charAt(i));
            if (i + 1 < originalText.length()) {
                builder.append("\u00A0");
            }
        }
        finalText = new SpannableString(builder.toString());
    } else {
        finalText = (SpannableString) originalText;
    }

    for (int i = 1; i < finalText.length(); i += 2) {
        finalText.setSpan(new ScaleXSpan((spacing + 1) / 10), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
    super.setText(finalText, TextView.BufferType.SPANNABLE);
}
Masked answered 1/10, 2015 at 9:44 Comment(0)
L
0

I wanted to use @PedroBarros answer, but by defining what the spacing should be in pixel.

Here's my edit to the applySpacing method :

private void applySpacing() {
    if (this == null || this.originalText == null) return;

    Paint testPaint = new Paint();
    testPaint.set(this.getPaint());
    float spaceOriginalSize = testPaint.measureText("\u00A0");
    float spaceScaleXFactor = ( spaceOriginalSize > 0 ? spacing/spaceOriginalSize : 1);

    StringBuilder builder = new StringBuilder();
    for(int i = 0; i < originalText.length(); i++) {
        builder.append(originalText.charAt(i));
        if(i+1 < originalText.length()) {
            builder.append("\u00A0");
        }
    }
    SpannableString finalText = new SpannableString(builder.toString());
    if(builder.toString().length() > 1) {
        for(int i = 1; i < builder.toString().length(); i+=2) {
            finalText.setSpan(new ScaleXSpan(spaceScaleXFactor), i, i+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }
    super.setText(finalText, BufferType.SPANNABLE);
}

I'm a beginner as an Android developer, please feel free to let me know if this is not good !

Lola answered 20/10, 2015 at 18:57 Comment(1)
this == null can it ever be?Pawl
C
0

One more solution.

public static SpannableStringBuilder getSpacedSpannable(Context context, String text, int dp) {
        if (text == null) return null;
        if (dp < 0) throw new RuntimeException("WRONG SPACING " + dp);
        Canvas canvas = new Canvas();
        Drawable drawable = ContextCompat.getDrawable(context, R.drawable.pixel_1dp);
        Bitmap main = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        canvas.setBitmap(main);
        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        drawable.draw(canvas);
        SpannableStringBuilder builder = new SpannableStringBuilder();
        char[] array = text.toCharArray();
        Bitmap bitmap = Bitmap.createScaledBitmap(main, dp * main.getWidth(), main.getHeight(), false);
        for (char ch : array) {
            builder.append(ch);
            builder.append(" ");
            ImageSpan imageSpan = new ImageSpan(context, bitmap);
            builder.setSpan(imageSpan, builder.length() - 1, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        return builder;
    }

Where pixel_1dp is XML:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="@android:color/transparent"/>
    <size android:height="1dp" android:width="1dp"/>

</shape>

To set spacing use code like this:

textView.setText(getSpacedSpannable(context, textView.getText().toString(), <Your spacing DP>), TextView.BufferType.SPANNABLE);
Caitlyncaitrin answered 21/3, 2017 at 4:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.