How to use support library fonts feature as a part of the TextView content (using spannable)?

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:

class CustomTypefaceSpan(private val typeface: Typeface?) : MetricAffectingSpan() {
    override fun updateDrawState(paint: TextPaint) {
        paint.typeface=typeface
    }

    override fun updateMeasureState(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 :

class CustomTypefaceSpan(val font: Typeface?) : MetricAffectingSpan() {
    override fun updateMeasureState(textPaint: TextPaint) = update(textPaint)
    override fun updateDrawState(textPaint: TextPaint?) = update(textPaint)

    private fun update(tp: TextPaint?) {
        tp.apply {
            val old = this!!.typeface
            val oldStyle = old?.style ?: 0
            val 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

<string name="title"><annotation font="lato_light">Hello</annotation> <annotation font="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 (annotation in 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.


Custom Class for apply fonrFamilySpan

 public class MultipleFamilyTypeface extends TypefaceSpan {
        private final Typeface typeFace;

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

        @Override
        public void updateDrawState(TextPaint ds) {
            applyTypeFace(ds, typeFace);
        }

        @Override
        public void updateMeasureState(TextPaint paint) {
            applyTypeFace(paint, typeFace);
        }

        private static void applyTypeFace(Paint paint, Typeface tf) {
            int oldStyle;
            Typeface old = paint.getTypeface();
            if (old == null) {
                oldStyle = 0;
            } else {
                oldStyle = old.getStyle();
            }

            int fake = 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

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        String firstWord = "Hello ";
        String secondWord = "Word ";
        String thirdWord = "Normal ";

        TextView textViewTest = findViewById(R.id.textViewTest);

        Spannable spannable = new SpannableString(firstWord + secondWord + thirdWord);

        Typeface CUSTOM_TYPEFACE = ResourcesCompat.getFont(this, R.font.akronim);
        Typeface SECOND_CUSTOM_TYPEFACE = ResourcesCompat.getFont(this, R.font.baloo_thambi);

        spannable.setSpan(new MultipleFamilyTypeface("akronim", CUSTOM_TYPEFACE), 0, firstWord.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        spannable.setSpan(new MultipleFamilyTypeface("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 = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        TextView textViewTest = findViewById(R.id.textViewTest);


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

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

    }


    public Spannable processToFontStyle(String text) {

        Document doc = Jsoup.parse(text);
        Elements tags = doc.getAllElements();
        String cleanText = doc.text();
        Log.d("ClearTextTag", "Text " + cleanText);
        Spannable spannable = new SpannableString(cleanText);
        List<String> tagsFromString = new ArrayList<>();
        List<Integer> startTextPosition = new ArrayList<>();
        List<Integer> endTextPosition = new ArrayList<>();
        for (Element tag : tags) {
            String nodeText = tag.text();
            if (myCustomTag.contains(tag.tagName())) {
                int startingIndex = cleanText.indexOf(nodeText);
                tagsFromString.add(tag.tagName());
                startTextPosition.add(startingIndex);
                endTextPosition.add(startingIndex + nodeText.length());
            }
        }

        Typeface CUSTOM_TYPEFACE = ResourcesCompat.getFont(this, R.font.akronim);
        Typeface SECOND_CUSTOM_TYPEFACE = ResourcesCompat.getFont(this, R.font.baloo_thambi);
        Typeface XYZ_CUSTOM_TYPEFACE = ResourcesCompat.getFont(this, R.font.architects_daughter);


        for (int i = 0; i < tagsFromString.size(); i++) {
            String fontName = tagsFromString.get(i);
            Typeface selected = 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(new MultipleFamilyTypeface(fontName, selected), startTextPosition.get(i), endTextPosition.get(i), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        }


        return spannable;
    }

OutPut

enter image description here


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

        public void onSuccess(@NonNull String title) {
                                            decorateTitle(title, R.id.listing_toolbar_title);
                                    }

TextUtils.java

public final class TextUtils {

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

    public static 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"));
        } else if (text.contains("_")) {
            return sanitize(text.split("_"));
        } else if (text.contains("-")) {
            return sanitize(text.split("-"));
        }
        if (!Character.isUpperCase(text.charAt(0))) {
            return text.substring(0, 1).toUpperCase() + text.substring(1);
        } else {
            return text;
        }
    }

    private static String sanitize(String[] strings) {
        StringBuilder sb = new StringBuilder();
        int lastIndex = strings.length - 1;
        for (int i = 0; i < strings.length; i++) {
            String str = 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();
    }


    public static String fillWithUnderscore(String text) {
        if (text.contains(" ")) {
            String[] splitText = text.split(" ");
            StringBuilder sb = new StringBuilder();
            int lastIndex = splitText.length - 1;
            for (int i = 0; i < splitText.length; i++) {
                sb.append(splitText[i]);
                if (i != lastIndex) sb.append("_");
            }
            return sb.toString();
        } else return text;
    }


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

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

    public static String sanitizeProductName(String productName) {
        if (productName.contains("\ufffd")) {
            return productName.replaceAll("\ufffd", "");
        } else return productName;
    }

    ///////////////////////////////////////////////////////////////////////////
    // String Spanning
    ///////////////////////////////////////////////////////////////////////////

    private static void applyCustomTypeFace(Paint paint, Typeface tf) {
        paint.setTypeface(tf);
    }

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

    public static SpannableStringBuilder stringSpanning(List<Option> options, String text) {
        SpannableStringBuilder spannable = new SpannableStringBuilder(text);
        for (Option option : options) {
            spannable.setSpan(new CustomTypefaceSpan(option.getFont()),
                    option.getFromIndex(), option.getToIndex(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
            spannable.setSpan(new ForegroundColorSpan(option.getColor()),
                    option.getFromIndex(), option.getToIndex(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
        }
        return spannable;
    }

    static class CustomTypefaceSpan extends MetricAffectingSpan {

        private final Typeface typeface;

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

        @Override
        public void updateDrawState(TextPaint ds) {
            applyCustomTypeFace(ds, typeface);
        }

        @Override
        public void updateMeasureState(TextPaint paint) {
            applyCustomTypeFace(paint, typeface);
        }
    }

    public static class Option {
        private Typeface font;
        private int color;
        private int fromIndex;
        private int toIndex;

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

        public Option(Context context, @FontRes int font, @ColorRes int 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;
        }

        public void setFont(Typeface font) {
            this.font = font;
        }

        public int getColor() {
            return color;
        }

        public void setColor(int color) {
            this.color = color;
        }

        public int getFromIndex() {
            return fromIndex;
        }

        public void setFromIndex(int fromIndex) {
            this.fromIndex = fromIndex;
        }

        public int getToIndex() {
            return toIndex;
        }

        public void setToIndex(int toIndex) {
            this.toIndex = toIndex;
        }
    }

    public static Double toDouble(String text) {
        StringBuilder collect = new StringBuilder();
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);
            if (Character.isDigit(c))
                collect.append(c);
        }
        return Double.parseDouble(collect.toString());
    }
}