web-dev-qa-db-fra.com

Comment ajuster le crénage du texte dans Android TextView?

Existe-t-il un moyen de régler l'espacement entre les caractères dans une Android TextView? Je crois que cela s'appelle typiquement "crénage".

Je connais l'attribut Android:textScaleX, mais cela compresse les caractères avec l'espacement.

24
emmby

Autant que je sache, vous ne pouvez pas ajuster le crénage dans TextView. Vous pourrez peut-être ajuster le crénage si vous dessinez vous-même le texte de la variable Canvas à l'aide des API graphiques 2D.

12
CommonsWare

J'ai construit une classe personnalisée qui étend TextView et ajoute une méthode "setSpacing". La solution de contournement est similaire à celle de @Noah. La méthode ajoute un espace entre chaque lettre de la chaîne et avec SpannedString modifie le TextScaleX des espaces, permettant ainsi un espacement positif et négatif.

J'espère que ça aide quelqu'un ^^

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

En l'utilisant:

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);
65
Pedro Barros

Si quelqu'un cherche un moyen simple d'appliquer le crénage à n'importe quelle chaîne (techniquement, CharSequence) sans utiliser de 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;
}
13
Takhion

Voici ma solution, qui ajoute un espacement uniforme (en pixels) entre chaque caractère. Cette étendue suppose que tout le texte est sur une seule ligne. Ceci implémente fondamentalement ce que @commonsWare suggère.

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

Le seul moyen que j'ai trouvé d'ajuster le crénage est de créer une police personnalisée dans laquelle l'avance du glyphe est modifiée.

3
mvds

Vous pouvez également essayer d’utiliser un SpannedString mais vous devrez l’analyser et changer l’espacement des caractères pour chacun des mots.

1
Noah

Cette réponse peut être utile pour quelqu'un qui souhaite dessiner du texte avec le crénage sur un canevas, en utilisant drawText (il ne s'agit pas de texte dans un TextView).

Depuis Lollipop, la méthode setLetterSpacing est disponible sur Paint. Si le SDK est Lollipop et activé, setLetterSpacing est utilisé. Sinon, une méthode est appelée qui fait quelque chose de similaire à la suggestion de @ dgmltn ci-dessus:

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

Il est difficile d'ajuster l'espacement entre les caractères lorsque vous utilisez TextView. Mais si vous pouvez gérer le dessin vous-même, il devrait y avoir un moyen de le faire.

Ma réponse à cette question est: utilisez votre Span personnalisé .

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

Expliquez:  

Construire votre propre Span peut vous aider à obtenir de nombreux effets étonnants, tels que rendre un TextView flou, changer l’arrière-plan ou le premier plan de votre TextView, voire même créer une animation. J'apprends beaucoup de ce post Transforme un concept puissant .

Comme vous ajoutez un espacement à chaque caractère, nous vous recommandons d'utiliser une étendue de base de niveau caractère. Dans ce cas, RemplacementSpan est le meilleur choix. J'ajoute une méthode spacing. Ainsi, lorsque vous l'utilisez, vous pouvez simplement laisser l'espace de votre choix pour chaque caractère en tant que paramètre.

Lors de la construction de votre étendue personnalisée, vous devez remplacer au moins deux méthodes, getSize et draw. La méthode getSize doit renvoyer la largeur finale après l'ajout de l'espacement pour toute la séquence, et à l'intérieur du bloc de la méthode draw, vous pouvez contrôler la Canvas pour faire le dessin souhaité.

Alors, comment utilisons-nous ce LetterSpacingSpan? C'est facile:

Utilisation:

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

Et c'est tout.

1
Anthonyeef

Je voulais utiliser la réponse @PedroBarros, mais en définissant ce que l'espacement devrait être en pixel.

Voici ma modification de la méthode 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);
}

Je suis un débutant en tant que développeur Android, n'hésitez pas à me faire savoir si ce n'est pas bon!

0
Olivier Berni

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

Où pixel_1dp est 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>

Pour définir l'espacement, utilisez le code suivant:

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

Il y a une petite modification de la réponse de @Pedro Barros. Il est utile si vous utilisez SpannableString pour le définir, par exemple. si vous voulez faire différentes couleurs de certains caractères:

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