Pregunta

¿Hay alguna manera de ajustar el espacio entre los caracteres en un TextView de Android? Creo que esto se suele llamar "kerning".

Soy consciente del atributo android: textScaleX , pero eso comprime los caracteres junto con el espaciado.

¿Fue útil?

Solución

AFAIK, no puede ajustar el interletraje en TextView . Es posible que pueda ajustar el interletraje si dibuja el texto en el Canvas usted mismo utilizando las API de gráficos 2D.

Otros consejos

Creé una clase personalizada que extiende TextView y agrega un método "setSpacing". La solución es similar a lo que dijo @Noah. El método agrega un espacio entre cada letra de la Cadena y con SpannedString cambios TextScaleX de los espacios, lo que permite un espaciado positivo y negativo.

Espero que ayude a alguien ^^

/**
 * 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;
    }
}

Utilizándolo:

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

Si alguien está buscando una manera simple de aplicar el interletraje a cualquier cadena (técnicamente, CharSequence ) sin usar un 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;
}

Aquí está mi solución, que agrega un espaciado uniforme (en píxeles) entre cada carácter. Este lapso asume que todo el texto está en una sola línea. Básicamente, esto implementa lo que sugiere @commonsWare.

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

La única forma en que encontré ajustar el interletraje es crear una fuente personalizada en la que se altere el avance del glifo.

También puede intentar usar un SpannedString pero necesitaría analizarlo y cambiar el espacio entre caracteres para cada una de las palabras

Esta respuesta puede ser útil para alguien que quiera dibujar texto con interletraje en un Canvas, usando drawText (no se trata de texto en un TextView).

Desde Lollipop, el método setLetterSpacing está disponible en Paint. Si el SDK es LOLLIPOP y está activado, se utiliza setLetterSpacing. De lo contrario, se invoca un método que hace algo similar a la sugerencia de @ dgmltn anterior:

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

Es difícil ajustar el espaciado entre caracteres cuando se utiliza TextView. Pero si puede manejar el dibujo usted mismo, debería haber alguna forma de hacerlo.

Mi respuesta a esta pregunta es: usa tu Span personalizado .

Mi código:

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

Explicar:

Construir su propio Span puede ayudarlo a lograr muchos efectos sorprendentes, como hacer un TextView borroso, cambiar el fondo o primer plano de su TextView, incluso hacer algo de animación. Aprendo mucho de esta publicación Span a concepto poderoso .

Debido a que está agregando espaciado a cada personaje, deberíamos usar un rango base de nivel de personaje, en este caso, ReplacementSpan es la mejor opción. Agrego un método espaciado , por lo que, al usarlo, simplemente puede pasar el espacio que desee para cada carácter como parámetro.

Al construir su espacio personalizado, debe anular al menos dos métodos, getSize y draw . El método getSize debería devolver el ancho final después de agregar el espacio para toda la secuencia, y dentro del bloque de método draw , puede controlar el Canvas para hacer el dibujo que quieras.

Entonces, ¿cómo usamos este LetterSpacingSpan? Es fácil:

Uso :

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

Y eso es todo.

Hay una pequeña edición de la respuesta de @Pedro Barros. Es útil si usa SpannableString para configurarlo, p. si quieres hacer diferentes colores de algunos personajes:

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

Quería usar la respuesta @PedroBarros, pero definiendo cuál debería ser el espaciado en píxeles.

Aquí está mi edición del método applySpacing:

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

Soy un principiante como desarrollador de Android, ¡no dudes en avisarme si esto no es bueno!

Una solución más.

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

Donde pixel_1dp es 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>

Para establecer el espaciado, use un código como este:

textView.setText(getSpacedSpannable(context, textView.getText().toString(), <Your spacing DP>), TextView.BufferType.SPANNABLE);

Desde Android 21, puede usar el atributo letterSpacing .

<TextView
    android:width="..."
    android:height="..."
    android:letterSpacing="1.3"/>
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top