6

The TextView.setLetterSpacing allows the letter spacing / character spacing to be set.

Is there a corresponding CharacterStyle / span class that allows the letter spacing to be set on a subset of the text in a TextView?

alexbirkett
  • 2,604
  • 2
  • 26
  • 30

3 Answers3

12

So I solved this by wring my own span class, it works on API level 21 and up.

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Parcel;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;

/**
 * Created by alex on 19/02/2015.
 */
@TargetApi(21)
public class LetterSpacingSpan extends MetricAffectingSpan {
    private float letterSpacing;

    /**
     * @param letterSpacing
     */
    public LetterSpacingSpan(float letterSpacing) {
        this.letterSpacing = letterSpacing;
    }

    public float getLetterSpacing() {
        return letterSpacing;
    }

    @Override
    public void updateDrawState(TextPaint ds) {
        apply(ds);
    }

    @Override
    public void updateMeasureState(TextPaint paint) {
        apply(paint);
    }

    private void apply(TextPaint paint) {
        paint.setLetterSpacing(letterSpacing);
    }

}
j__m
  • 9,392
  • 1
  • 32
  • 56
alexbirkett
  • 2,604
  • 2
  • 26
  • 30
  • What problem does this solve? If it's API level 21 and up you can just set the letterSpacing on the TextView directly. – qtyq Dec 01 '17 at 01:25
  • 3
    This allows you to set a letterSpacing on a span (a portion of the text). If you set letterSpacing on the whole TextView it will be applied to the whole TextView. – alexbirkett Dec 03 '17 at 09:30
7

You can take a look at this custom class implementation that implements what you need.

EDIT: Ok, if you want to apply to a small portion of the text you can do this:

CharSequence firstPart = "First Part";
CharSequence thirdPart = "Third Part";
SpannableStringBuilder middlePart = new SpannableStringBuilder("Middle Part");

int spacing = 2;
final String nonBreakingSpace = "\u00A0";


for (int i = middlePart.length() - 1; i >= 1; i--){
    middlePart.insert(i, nonBreakingSpace);
    middlePart.setSpan(new ScaleXSpan(spacing), i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}

((TextView) rootView.findViewById(R.id.text_view)).setText(TextUtils.concat(firstPart, " ", middlePart, " ", thirdPart));

It's not perfect though.

EDIT:

To be clear, this is the outcome of the code above

The first line ("Custom Spannable") is the result of (+/-) the code above. The second line ("Letter Spacing") is the result of the property android:letterSpacing="1".

enter image description here

As you can see, and as I said, it's not perfect, but is the only solution I found for pre-lollipop devices.

I also thought the whole point of your question was to make this compatible with pre-lollipop devices. My bad.

Cheers

Community
  • 1
  • 1
Joao Sousa
  • 4,055
  • 1
  • 26
  • 29
  • That's solution appears to be setting the spacing on the whole ```TextView```not the subset of the text using a CharacterStyle / span – alexbirkett Feb 17 '15 at 14:02
  • I suspect that ```ScaleXSpan``` is analogous to [```setTextScaleX```](http://developer.android.com/reference/android/widget/TextView.html#setTextScaleX(float)) not [```setLetterSpacing```](http://developer.android.com/reference/android/widget/TextView.html#setLetterSpacing(float)) – alexbirkett Feb 17 '15 at 21:37
  • I don't think so. `setLetterSpacing` *should* adjust the kerning, while `setTextScaleX` "stretches" horizontally text view. – Joao Sousa Feb 17 '15 at 21:40
  • I've just tested this, ```ScaleXSpan``` stretches the text I want to set the letter spacing. – alexbirkett Feb 18 '15 at 07:36
  • If you had tested this, you would have got the same final result that I got. I even attached a screenshot to prove it. – Joao Sousa Feb 19 '15 at 12:34
  • Which API level ```setLetterSpacing``` is not relevant to the question really. I removed it to avoid confusing others – alexbirkett Feb 19 '15 at 12:49
  • I can see how this works now. You're setting the ScaleXSpan on each space added between the letters! That's a lot of spans! Removed my downvote anyway – alexbirkett Feb 19 '15 at 12:51
  • exactly. But for API >= 21 your solution is quite neat ;) I believe you can mark it as accepted answer. – Joao Sousa Feb 19 '15 at 12:54
  • This approach corrupts both UTF-16 surrogate pairs and provides text-shaping from correctly doing ligatures. – kusma Nov 10 '15 at 10:40
1

You can use this function that I have written in Kotlin. It will allow you to modify letter spacing between two letters in one TextView without other letters being affected.

fun SpannableStringBuilder.setLetterSpacingBetweenTwoLettersSpans(
    firstLetter: Char,
    secondLetter: Char,
    spacing: Float
) {
    forEachIndexed { charIndex, startingChar ->
        if (startingChar == firstLetter && get(charIndex + 1) == secondLetter) {
            var negative = spacing < 0
            if (get(lastIndex) != ' ') append(' ')
            forEachIndexed { index, _ ->
                if (index > charIndex) {
                    val appliedSpacing = if (index == charIndex + 1) {
                        if (negative) -abs(spacing) else spacing
                    } else {
                        if (negative) spacing else -spacing
                    }
                    if (index != lastIndex) {
                        setSpan(object : MetricAffectingSpan() {
                            override fun updateMeasureState(textPaint: TextPaint) { apply(textPaint) }
                            override fun updateDrawState(tp: TextPaint?) { tp?.let { apply(tp) } }
                            private fun apply(paint: TextPaint) { paint.letterSpacing = appliedSpacing }
                        }, index, index + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
                    }
                    negative = !negative
                }
            }
        }
    }
}

So you can use it like this:

val spannableStringBuilder = SpannableStringBuilder()
spannableStringBuilder.append("Bear")
spannableStringBuilder.setLetterSpacingBetweenTwoLettersSpans(
    firstLetter = "e",
    secondLetter = "a"
    spacing = 1f // Negative works aswell (that's why I initially did it)
)
yourTextView.text = spannableStringBuilder

1f will look as if you'd put a space between the two letters I think. Negative will work aswell.

Stanislav Kinzl
  • 370
  • 4
  • 6