如何使用支持库字体功能作为 TextView 内容的一部分(使用 spannable)?

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

背景

支持库(文档 here)允许您使用 "res/font" 文件夹中的 TTF 字体文件,或者 XML :

app:fontFamily="@font/lato_black"

或通过代码:

val typeface = ResourcesCompat.getFont(context, R.font.lato_black)

问题

虽然我知道可以使用 spannable 技术在部分 TextView 内容中设置不同的样式(例如粗体、斜体、颜色等...),但我发现设置不同的唯一方法字体,是通过使用 OS 的内置字体,如图 here 所示,但我看不到加载字体的新方法。

我试过的

我试图找到一种在两者之间进行转换的方法,但没有成功。当然,我也尝试在文档中寻找可能的功能,我也尝试在网上找到它。

问题

如何为TextView的不同部分设置不同的字体?

例如,在文本"Hello world"中,设置"Hello"的字体为"lato_black",其余默认。


编辑:因为我从 2 个不同的人那里得到了大致相同的答案,所以我不能接受其中一个。为他们的努力给了他们+1,我稍微改变了问题:

我如何轻松地将字体样式设置为文本的一部分,同时让 strings.xml 文件使用自定义字体标签定义它。

例如,这可以在 strings.xml 文件中按照我上面的要求进行设置:

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

然后,在代码中,您要做的就是使用如下内容:

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

我认为这很重要,因为翻译后的字符串可能与英文的差异太大,因此您不能只解析字符串的文本然后选择在何处设置自定义字体。它必须在字符串文件中。

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

应用字体

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

编辑自定义标签的方法二

在gradle

中添加implementation 'org.jsoup:jsoup:1.11.3'
 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

试试这个...它在我的情况下工作得很好,你可以根据你的要求进行更改。 目前,它的工作原理是,例如:- Hello Worldfont lato_light 中的 Hellofont lato_bold 中的 remaining

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

在Javaclass添加这个方法passString 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());
    }
}

由于 MJMTheMatrix 的两个答案几乎相同(但对我来说过于复杂)并且两个答案大约在同一时间,所以我不能只选择其中一个,所以我为每个人都授予了 +1,要求他们缩短它但支持 XML 标签以便于处理字符串文件。

现在,这里是如何为 TextView 中的部分文本设置自定义字体的更短版本:

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

    override fun updateMeasureState(paint: TextPaint) {
        paint.typeface=typeface
    }
}

示例用法:

    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

编辑:似乎 Google 提供了有关此的视频,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
        }
    }
}

视频 here 中也谈到了 strings.xml 中处理它的解决方案,但使用注释而不是新的 HTML 标签。示例:

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

结果:

我仍然很确定可以使用 fromHtml,但它可能不值得。

我也想知道如果我们想同时使用基本的 HTML 标签和我们为字体设置的自定义标签,如果我们确实在那里使用 annotation,应该怎么做。

扩展 android 开发者的 , 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)
}

那么你可以使用:

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

不要使用spannable.toString()

奖金:fontSize Spannable

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