如何在TextView的同一部分文本上设置多个Span?

70

假设我有以下文本:

Hello stackOverflow

我希望将第二个单词同时设置为 RelativeSizeSpan(以设置相对字体大小)和TextAppearanceSpan(以设置文本颜色), 我该如何合并它们?

我只知道我可以选择其中一个,例如使用以下代码:

final SpannableString textToShow = new SpannableString("Hello stackOverflow");
textToShow.setSpan(new RelativeSizeSpan(1.5f), textToShow.length() - "stackOverflow".length(),textToShow.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(textToShow);

但我还需要设置颜色,甚至从其他跨度类中添加其他功能...

我该怎么办?


请参见 https://dev59.com/V3I_5IYBdhLWcg3wFvDL#41953808 - Suragch
5个回答

106

只需设置额外的span,当需要时它们将重叠/合并。这段代码对我有用:

final SpannableString text = new SpannableString("Hello stackOverflow");
text.setSpan(new RelativeSizeSpan(1.5f), text.length() - "stackOverflow".length(), text.length(),
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.RED), 3, text.length() - 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(text);

8
我脑海中有一个随机数,这只是为了展示您可以拥有多个相交的跨度。 - Zielony
4
但是我该如何添加多个ForegroundcolorSpan呢? - Arda Kara
@Zielony 如果我创建了一个新类,该怎么办?它应该扩展ReplacementSpan还是其他什么,以便我能够在同一文本上放置多个类的实例? - android developer
据我所知,算法不关心类型。 - Zielony
可以在此链接中找到详细的示例。 - Neeraj
还要注意,如果没有 textView.movementMethod = LinkMovementMethod.getInstance()ClickableSpan 将无法正常工作。 - Bugs Happen

43

我知道这是对已经回答的问题的新回复,但我想分享一个我制作的实用类,它可以使这个任务更加容易。


Java版本

public class SimpleSpanBuilder {
    private class SpanSection{
        private final String text;
        private final int startIndex;
        private final CharacterStyle[] styles;

        private SpanSection(String text, int startIndex,CharacterStyle... styles){
            this.styles = styles;
            this.text = text;
            this.startIndex = startIndex;
        }

        private void apply(SpannableStringBuilder spanStringBuilder){
            if (spanStringBuilder == null) return;
            for (CharacterStyle style : styles){
                spanStringBuilder.setSpan(style, startIndex, startIndex + text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
            }
        }
    }

    private List<SpanSection> spanSections;
    private StringBuilder stringBuilder;

    public SimpleSpanBuilder(){
        stringBuilder = new StringBuilder();
        spanSections = new ArrayList<>();
    }

    public SimpleSpanBuilder append(String text,CharacterStyle... styles){
        if (styles != null && styles.length > 0) {
            spanSections.add(new SpanSection(text, stringBuilder.length(),styles));
        }
        stringBuilder.append(text);
        return this;
    }

    public SimpleSpanBuilder appendWithSpace(String text,CharacterStyle... styles){
        return append(text.concat(" "),styles);
    }

    public SimpleSpanBuilder appendWithLineBreak(String text,CharacterStyle... styles){
        return append(text.concat("\n"),styles);
    }

    public SpannableStringBuilder build(){
        SpannableStringBuilder ssb = new SpannableStringBuilder(stringBuilder.toString());
        for (SpanSection section : spanSections){
            section.apply(ssb);
        }
        return ssb;
    }

    @Override
    public String toString() {
        return stringBuilder.toString();
    }
}

使用方法:

SimpleSpanBuilder ssb = new SimpleSpanBuilder();
ssb.appendWithSpace("Hello");
ssb.append("StackOverflow",new ForegroundColorSpan(Color.RED),new RelativeSizeSpan(1.5));
textView.setText(ssb.build());

Kotlin 版本

class SimpleSpanBuilder() {

    class Span {
        private var startIndex: Int = 0
        internal var text: String
        private var styles: Array<out CharacterStyle>

        internal constructor(index: Int, text: String, vararg styles: CharacterStyle) {
            this.startIndex = index
            this.text = text
            this.styles = styles
        }

        constructor(text: String, vararg styles: CharacterStyle) : this(0, text, *styles)

        internal fun setIndex(index: Int): Span {
            return Span(index, this.text, *this.styles)
        }

        internal fun apply(spanStringBuilder: SpannableStringBuilder?) {
            if (spanStringBuilder == null) return
            for (style in styles) {
                spanStringBuilder.setSpan(
                    style,
                    startIndex,
                    startIndex + text.length,
                    Spannable.SPAN_INCLUSIVE_EXCLUSIVE
                )
            }
        }
    }

    private val spanSections = mutableListOf<Span>()
    private val stringBuilder = StringBuilder()

    constructor(text: String, vararg styles: CharacterStyle) : this() {
        plus(Span(text, *styles))
    }

    operator fun plus(span: SimpleSpanBuilder.Span): SimpleSpanBuilder {
        spanSections.add(span.setIndex(stringBuilder.length))
        stringBuilder.append(span.text)
        return this
    }

    fun build(): SpannableStringBuilder {
        val ssb = SpannableStringBuilder(stringBuilder.toString())
        for (section in spanSections) {
            section.apply(ssb)
        }
        return ssb
    }

    override fun toString(): String {
        return stringBuilder.toString()
    }
}

使用方法

var ssb = SimpleSpanBuilder("Hello ",ForegroundColorSpan(Color.BLUE))
ssb += SimpleSpanBuilder.Span(
    "StackOverflow",
    ForegroundColorSpan(Color.RED),
    RelativeSizeSpan(1.5f)
)

textView.text = ssb.build()

6
这实际上是一个不错的解决方案。为这份努力点一赞。 - android developer
如何在文本上设置点击监听器。 - Prinkal Kumar
@PrinkalKumar 请尝试使用ClickableSpan更新的答案。 - W.K.S
请注意,如果没有 textView.movementMethod = LinkMovementMethod.getInstance(),则 ClickableSpan 将无法正常工作。 - Bugs Happen
你好,你支持图片吗? - famfamfam

8

最简单的方法?

textView.setText("我爱编程");

setHighLightedText(textView,"编程");

只需使用以下方法 -

public void setHighLightedText(TextView tv, String textToHighlight) {
    String tvt = tv.getText().toString();
    int ofe = tvt.indexOf(textToHighlight, 0);
    Spannable wordToSpan = new SpannableString(tv.getText());

    for (int ofs = 0; ofs < tvt.length() && ofe != -1; ofs = ofe + 1) {
        ofe = tvt.indexOf(textToHighlight, ofs);
        if (ofe == -1)
            break;
        else {
            // you can change or add more span as per your need
            wordToSpan.setSpan(new RelativeSizeSpan(2f), ofe,ofe + textToHighlight.length(), 0); // set size
            wordToSpan.setSpan(new ForegroundColorSpan(Color.RED), ofe, ofe + textToHighlight.length(), 0);// set color
            tv.setText(wordToSpan, TextView.BufferType.SPANNABLE);
        }
    }
}

1
这很好,但我已经接受了一个完美运作的答案。不过,我会因为你的努力给你加上一分。 - android developer
1
这应该是被接受的答案!运行得很好,谢谢! - Ensar Bayhan

4

Kotlin可以通过对SpannableStringBuilder进行扩展来帮助实现这一点:

fun SpannableStringBuilder.spansAppend(
    text: CharSequence,
    flags: Int,
    vararg spans: Any
): SpannableStringBuilder {
    val start = length
    append(text)

    spans.forEach { span ->
        setSpan(span, start, length, flags)
    }

    return this
}

使用示例:

val builder = SpannableStringBuilder()
builder.append("Start of string ")
builder.spansAppend(
    "text spanned",
    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
    RelativeSizeSpan(1.1f),
    ForegroundColorSpan(Color.RED)
)

1
你能否展示一两个使用你的代码的样例吗?我不确定它具体是做什么的... - android developer
1
@androiddeveloper 我已经添加了一个例子。 - Kevin Robatel
1
哦,这非常好。感谢您的工作和努力。有一天可能会非常有帮助。当我需要它时,我希望能再次查看它。如此短小的代码实现了如此有用的功能。我会为此点赞。 - android developer
等一下,这是一个已经存在的函数?今天有人给我写了这个:https://dev59.com/uVUL5IYBdhLWcg3wopQM#52331300 - android developer
不知道这个。inSpan 是来自 Android-KTX https://github.com/android/android-ktx/blob/master/src/main/java/androidx/core/text/SpannableStringBuilder.kt - Kevin Robatel

0
我在Java中编写了一个示例函数,可以同时追加多个span标签。
private void append(SpannableStringBuilder builder, CharSequence text, int flags, Object... spans) {
    int start = builder.length();
    builder.append(text);
    if (spans != null) {
        for (Object span :  spans) {
            if (span != null)
                builder.setSpan(span, start, builder.length(), flags);
        }
    }
}

使用这个方法很简单:

SpannableStringBuilder builder = new SpannableStringBuilder();
append(builder, "test", Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, new ForegroundColorSpan(Color.WHITE), new StyleSpan(android.graphics.Typeface.BOLD));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接