How to use support library fonts feature as a part

2019-01-25 13:12发布

问题:

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 = ResourcesCompat.getFont(context, R.font.lato_black)

The problem

While I know it's possible to use spannable technique to set different styles in parts of the TextView content (such as bold, italic, colors,etc...) , the only thing I've found for setting a different font, is by using the built in fonts of the OS, as shown here, but I can't see a way to do it for the new way to load fonts.

What I've tried

I tried to find a way to convert between the two, but with no luck. Of course, I also tried to find in the docs of possible functions, and I've tried to find about it over the Internet.

The question

How to set different font for different parts of the TextView ?

For example, in the text "Hello world", set the "Hello" to have the font of "lato_black" , and the default for the rest.


EDIT: Since I got about the same answer from 2 different people, I can't accept one over the other. Gave them both +1 for the effort, and I've changed the question a bit:

How would I set the font style to a part of the text, easily, while having the strings.xml file define it using a customized font-tag.

For example, this could be in the strings.xml file to set it as I've asked above :

<string name="something" ><customFont fontResource="lato_black">Hello</customFont> world</string>

Then, in code, all you would do is use something like:

textView.setText (Html.fromHtml(text, null, CustomFontTagHandler()))

I think it's very important, because translated strings might have become too different from what's in English, so you can't just parse the text of the string and then choose where to set the custom font. It has to be inside the strings file.

回答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:

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:

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.



回答2:

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

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



回答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

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


回答4:

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

inline fun 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:

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