Skip to content Skip to sidebar Skip to footer

How To Use Support Library Fonts Feature As A Part Of The Textview Content (using Spannable)?

Background The support library (docs here) allows you to use TTF fonts files in the 'res/font' folder, either in XML : app:fontFamily='@font/lato_black' or via code: val typeface

Solution 1:

Since both answers of MJM and TheMatrix are practically the same (yet over-complex for me) and both were answered around the same time, I couldn't just choose one of them, so I granted +1 for each, asking them to make it shorter yet support XML tag for easier handling with strings file.

For now, here's the much shorter version of how to set a custom font for a part of the text in the TextView:

classCustomTypefaceSpan(privateval typeface: Typeface?) : MetricAffectingSpan() {
    overridefunupdateDrawState(paint: TextPaint) {
        paint.typeface=typeface
    }

    overridefunupdateMeasureState(paint: TextPaint) {
        paint.typeface=typeface
    }
}

Sample usage :

val text = "Hello world"val index = text.indexOf(' ')
    val spannable = SpannableStringBuilder(text)
    spannable.setSpan(CustomTypefaceSpan(ResourcesCompat.getFont(this, R.font.lato_light)), 0, index, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
    spannable.setSpan(CustomTypefaceSpan(ResourcesCompat.getFont(this, R.font.lato_bold)), index, text.length, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
    textView.text = spannable

EDIT: seems Google provided a video about this, here :

classCustomTypefaceSpan(val font: Typeface?) : MetricAffectingSpan() {
    overridefunupdateMeasureState(textPaint: TextPaint) = update(textPaint)
    overridefunupdateDrawState(textPaint: TextPaint?) = update(textPaint)

    privatefunupdate(tp: TextPaint?) {
        tp.apply {
            val old = this!!.typeface
            val oldStyle = old?.style ?: 0val font = Typeface.create(font, oldStyle)
            typeface = font
        }
    }
}

And the solution of handling it in strings.xml is also talked about on the video, here , yet using annotations instead of new HTML tags. Example:

strings.xml

<stringname="title"><annotationfont="lato_light">Hello</annotation><annotationfont="lato_bold">world</annotation></string>

MainActivity.kt

val titleText = getText(R.string.title) as SpannedString
    val spannable = SpannableStringBuilder(titleText)
    val annotations = titleText.getSpans(0, titleText.length, android.text.Annotation::class.java)
    for (annotationin annotations) {
        if(annotation.key=="font"){
            val fontName=annotation.value
            val typeface= ResourcesCompat.getFont(this@MainActivity,resources.getIdentifier(fontName,"font",packageName))
            spannable.setSpan(CustomTypefaceSpan(typeface),spannable.getSpanStart(annotation),spannable.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        }
    }
    textView.text = spannable

And the result:

enter image description here

Still I'm pretty sure it's possible to use fromHtml, but it's probably not worth it.

I also wonder what should be done if we want to use both the basic HTML tags and the cusomzied one we've set for font, if we indeed use annotation there.

Solution 2:

Custom Class for apply fonrFamilySpan

publicclassMultipleFamilyTypefaceextendsTypefaceSpan {
        privatefinal Typeface typeFace;

        publicMultipleFamilyTypeface(String family, Typeface type) {
            super(family);
            typeFace = type;
        }

        @OverridepublicvoidupdateDrawState(TextPaint ds) {
            applyTypeFace(ds, typeFace);
        }

        @OverridepublicvoidupdateMeasureState(TextPaint paint) {
            applyTypeFace(paint, typeFace);
        }

        privatestaticvoidapplyTypeFace(Paint paint, Typeface tf) {
            int oldStyle;
            Typefaceold= paint.getTypeface();
            if (old == null) {
                oldStyle = 0;
            } else {
                oldStyle = old.getStyle();
            }

            intfake= oldStyle & ~tf.getStyle();
            if ((fake & Typeface.BOLD) != 0) {
                paint.setFakeBoldText(true);
            }

            if ((fake & Typeface.ITALIC) != 0) {
                paint.setTextSkewX(-0.25f);
            }

            paint.setTypeface(tf);
        }
    }

Apply Font

publicclassMainActivityextendsAppCompatActivity {

    @OverrideprotectedvoidonCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        StringfirstWord="Hello ";
        StringsecondWord="Word ";
        StringthirdWord="Normal ";

        TextViewtextViewTest= findViewById(R.id.textViewTest);

        Spannablespannable=newSpannableString(firstWord + secondWord + thirdWord);

        TypefaceCUSTOM_TYPEFACE= ResourcesCompat.getFont(this, R.font.akronim);
        TypefaceSECOND_CUSTOM_TYPEFACE= ResourcesCompat.getFont(this, R.font.baloo_thambi);

        spannable.setSpan(newMultipleFamilyTypeface("akronim", CUSTOM_TYPEFACE), 0, firstWord.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        spannable.setSpan(newMultipleFamilyTypeface("baloo_thambi", SECOND_CUSTOM_TYPEFACE), firstWord.length(), firstWord.length() + secondWord.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);


        textViewTest.setText(spannable);
    }
}

OutPut

enter image description here

Edit Method two for Custom tags

Add implementation 'org.jsoup:jsoup:1.11.3' in gradle

 List<String> myCustomTag = newArrayList<>();
    @OverrideprotectedvoidonCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        TextViewtextViewTest= findViewById(R.id.textViewTest);


        // mention list custom tag that you used 
        myCustomTag.add("akronim");
        myCustomTag.add("baloo_thambi");
        myCustomTag.add("xyz");

        Stringhtml="<akronim>Hello</akronim>"
                + "<baloo_thambi> Word  </baloo_thambi>"
                + " Normal "
                + " <xyz> testing </xyz> "
                + "<akronim>Styles</akronim>";
        textViewTest.setText(processToFontStyle(html));

    }


    public Spannable processToFontStyle(String text) {

        Documentdoc= Jsoup.parse(text);
        Elementstags= doc.getAllElements();
        StringcleanText= doc.text();
        Log.d("ClearTextTag", "Text " + cleanText);
        Spannablespannable=newSpannableString(cleanText);
        List<String> tagsFromString = newArrayList<>();
        List<Integer> startTextPosition = newArrayList<>();
        List<Integer> endTextPosition = newArrayList<>();
        for (Element tag : tags) {
            StringnodeText= tag.text();
            if (myCustomTag.contains(tag.tagName())) {
                intstartingIndex= cleanText.indexOf(nodeText);
                tagsFromString.add(tag.tagName());
                startTextPosition.add(startingIndex);
                endTextPosition.add(startingIndex + nodeText.length());
            }
        }

        TypefaceCUSTOM_TYPEFACE= ResourcesCompat.getFont(this, R.font.akronim);
        TypefaceSECOND_CUSTOM_TYPEFACE= ResourcesCompat.getFont(this, R.font.baloo_thambi);
        TypefaceXYZ_CUSTOM_TYPEFACE= ResourcesCompat.getFont(this, R.font.architects_daughter);


        for (inti=0; i < tagsFromString.size(); i++) {
            StringfontName= tagsFromString.get(i);
            Typefaceselected=null;
            switch (fontName) {
                case"akronim":
                    selected = CUSTOM_TYPEFACE;
                    break;
                case"baloo_thambi":
                    selected = SECOND_CUSTOM_TYPEFACE;
                    break;
                case"xyz":
                    selected = XYZ_CUSTOM_TYPEFACE;
                    break;
            }
            if (selected != null)
                spannable.setSpan(newMultipleFamilyTypeface(fontName, selected), startTextPosition.get(i), endTextPosition.get(i), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        }


        return spannable;
    }

OutPut

enter image description here

Solution 3:

Try this... it's working perfectly in my case, u can change according to your requirement. currently, It's working as, an example:- Hello World, Hello in font lato_light and remaining in font lato_bold.

protected final SpannableStringBuilder decorateTitle(String text, @IdRes int view) {
                            List<TextUtils.Option> options = new ArrayList<>();
                            int index = text.indexOf(' ');
                            if (index >= 0) {
                                options.add(new TextUtils.Option(ResourcesCompat.getFont(this, R.font.lato_light),
                                        ContextCompat.getColor(this, R.color.toolbar_title_text),
                                        0, index));
                                options.add(new TextUtils.Option(ResourcesCompat.getFont(this, R.font.lato_bold),
                                        ContextCompat.getColor(this, R.color.primary_text),
                                        index, text.length()));
                            } else options.add(new TextUtils.Option(ResourcesCompat.getFont(this, R.font.lato_bold),
                                    ContextCompat.getColor(this, R.color.primary_text),
                                    0, text.length()));

                            SpannableStringBuilder stringBuilder = TextUtils.stringSpanning(options, text);
                            if (view != 0) {
                                ((TextView) findViewById(view)).setText(stringBuilder);
                            }
                            return stringBuilder;
                        }

In Java class add this method pass String u want to decorate & view in xml

publicvoidonSuccess(@NonNull String title) {
                                            decorateTitle(title, R.id.listing_toolbar_title);
                                    }

TextUtils.java

publicfinalclassTextUtils {

    publicstatic String trim(String text) {
        text = text.trim();
        return text.replaceAll("\\s+", " ");
    }

    publicstatic String sanitize(String text) {
        if (text == null || text.isEmpty()) return text;

        if (text.contains("\ufffd")) {
            text = text.replaceAll("\ufffd", "");
        }

        if (text.contains(" ")) {
            return sanitize(text.split("\\s"));
        } elseif (text.contains("_")) {
            return sanitize(text.split("_"));
        } elseif (text.contains("-")) {
            return sanitize(text.split("-"));
        }
        if (!Character.isUpperCase(text.charAt(0))) {
            return text.substring(0, 1).toUpperCase() + text.substring(1);
        } else {
            return text;
        }
    }

    privatestatic String sanitize(String[] strings) {
        StringBuildersb=newStringBuilder();
        intlastIndex= strings.length - 1;
        for (inti=0; i < strings.length; i++) {
            Stringstr= strings[i];
            if (str.length() > 0) {
                if (Character.isLetter(str.charAt(0))
                        && !Character.isUpperCase(str.charAt(0))) {
                    sb.append(str.substring(0, 1).toUpperCase()).append(str.substring(1));
                } else {
                    sb.append(str);
                }

                if (i != lastIndex) sb.append(" ");
            }
        }
        return sb.toString();
    }


    publicstatic String fillWithUnderscore(String text) {
        if (text.contains(" ")) {
            String[] splitText = text.split(" ");
            StringBuildersb=newStringBuilder();
            intlastIndex= splitText.length - 1;
            for (inti=0; i < splitText.length; i++) {
                sb.append(splitText[i]);
                if (i != lastIndex) sb.append("_");
            }
            return sb.toString();
        } elsereturn text;
    }


    publicstatic String sanitizePrice(Double price) {
        if (Objects.isNull(price) || price == 0) return"";

        Stringpricing= String.format(Locale.getDefault(), "₹ %.0f", price);
        StringBuilderinput=newStringBuilder(pricing).reverse();
        StringBuilderoutput=newStringBuilder("");
        char[] digits = input.toString().toCharArray();
        for (inti=0; i < digits.length; i++) {
            if (i < 3 || i % 2 == 0) {
                output.append(digits[i]);
            } elseif (i % 2 != 0) {
                output.append(" ").append(digits[i]);
            }
        }
        return output.reverse().toString();
    }

    publicstatic String sanitizeProductName(String productName) {
        if (productName.contains("\ufffd")) {
            return productName.replaceAll("\ufffd", "");
        } elsereturn productName;
    }

    ///////////////////////////////////////////////////////////////////////////// String Spanning///////////////////////////////////////////////////////////////////////////privatestaticvoidapplyCustomTypeFace(Paint paint, Typeface tf) {
        paint.setTypeface(tf);
    }

    publicstatic SpannableStringBuilder stringSpanning(List<Option> options, StringBuilder builder) {
        return stringSpanning(options, builder.toString());
    }

    publicstatic SpannableStringBuilder stringSpanning(List<Option> options, String text) {
        SpannableStringBuilderspannable=newSpannableStringBuilder(text);
        for (Option option : options) {
            spannable.setSpan(newCustomTypefaceSpan(option.getFont()),
                    option.getFromIndex(), option.getToIndex(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
            spannable.setSpan(newForegroundColorSpan(option.getColor()),
                    option.getFromIndex(), option.getToIndex(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
        }
        return spannable;
    }

    staticclassCustomTypefaceSpanextendsMetricAffectingSpan {

        privatefinal Typeface typeface;

        CustomTypefaceSpan(Typeface typeface) {
            this.typeface = typeface;
        }

        @OverridepublicvoidupdateDrawState(TextPaint ds) {
            applyCustomTypeFace(ds, typeface);
        }

        @OverridepublicvoidupdateMeasureState(TextPaint paint) {
            applyCustomTypeFace(paint, typeface);
        }
    }

    publicstaticclassOption {
        private Typeface font;
        privateint color;
        privateint fromIndex;
        privateint toIndex;

        publicOption(Typeface font, int color, int fromIndex, int toIndex) {
            this.font = font;
            this.color = color;
            this.fromIndex = fromIndex;
            this.toIndex = toIndex;
        }

        publicOption(Context context, @FontResint font, @ColorResint color, int fromIndex, int toIndex) {
            this.font = ResourcesCompat.getFont(context, font);
            this.color = ContextCompat.getColor(context, color);
            this.fromIndex = fromIndex;
            this.toIndex = toIndex;
        }

        public Typeface getFont() {
            return font;
        }

        publicvoidsetFont(Typeface font) {
            this.font = font;
        }

        publicintgetColor() {
            return color;
        }

        publicvoidsetColor(int color) {
            this.color = color;
        }

        publicintgetFromIndex() {
            return fromIndex;
        }

        publicvoidsetFromIndex(int fromIndex) {
            this.fromIndex = fromIndex;
        }

        publicintgetToIndex() {
            return toIndex;
        }

        publicvoidsetToIndex(int toIndex) {
            this.toIndex = toIndex;
        }
    }

    publicstatic Double toDouble(String text) {
        StringBuildercollect=newStringBuilder();
        for (inti=0; i < text.length(); i++) {
            charc= text.charAt(i);
            if (Character.isDigit(c))
                collect.append(c);
        }
        return Double.parseDouble(collect.toString());
    }
}

Solution 4:

To extend android developer's answer, one can make .font(){...} extension function like .bold{}, .backgroundColor{} from android ktx:

inlinefun SpannableStringBuilder.font(typeface: Typeface, builderAction: SpannableStringBuilder.() -> Unit): SpannableStringBuilder {
    return inSpans(TypefaceSpan(typeface), builderAction = builderAction)
}

Then you can use:

val spannable = SpannableStringBuilder()
                .append(getString(...))
                .font(ResourcesCompat.getFont(context!!, R.font.myFont)!!) {
                    append(getString(...))
                }
                .bold{append(getString(...))}
textView.text = spannable

Don't use spannable.toString().

Bonus: fontSize for Spannable:

inlinefun SpannableStringBuilder.fontSize(fontSize: Int, builderAction: SpannableStringBuilder.() -> Unit): SpannableStringBuilder {
    return inSpans(AbsoluteSizeSpan(fontSize), builderAction = builderAction)
}

Post a Comment for "How To Use Support Library Fonts Feature As A Part Of The Textview Content (using Spannable)?"