将 Spannable 与 String.format() 结合使用

Posted

技术标签:

【中文标题】将 Spannable 与 String.format() 结合使用【英文标题】:Combining Spannable with String.format() 【发布时间】:2014-01-23 02:03:23 【问题描述】:

假设你有以下字符串:

String s = "The cold hand reaches for the %1$s %2$s Ellesse's";
String old = "old"; 
String tan = "tan"; 
String formatted = String.format(s,old,tan); //"The cold hand reaches for the old tan Ellesse's"

假设您想以这个字符串结尾,但还为任何被String.format 替换的单词设置了一个特定的Span

例如,我们还想做以下事情:

Spannable spannable = new SpannableString(formatted);
spannable.setSpan(new StrikethroughSpan(), oldStart, oldStart+old.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ForegroundColorSpan(Color.BLUE), tanStart, tanStart+tan.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

是否有可靠的方法来了解oldtan 的起始索引?

请注意,仅搜索 'old' 会返回 'cold' 中的 'old',所以这不起作用。

我猜起作用的是事先搜索%[0-9]$s,并计算偏移量以解释String.format 中的替换。不过,这似乎很让人头疼,我怀疑可能有像String.format 这样的方法可以提供更多关于其格式细节的信息。嗯,有吗?

【问题讨论】:

至少对于颜色,您可以尝试替换字符串<font color='blue'>tan</font> 并使用 spannable=html.fromHtml(formatted)。不过,<strike>old</strike> 不会渲染。 使用TextUtils.expandTemplate 几乎可以肯定。 【参考方案1】:

像这样使用 Spannables 令人头疼——这可能是最直接的方法:

String s = "The cold hand reaches for the %1$s %2$s Ellesse's";
String old = "<font color=\"blue\">old</font>"; 
String tan = "<strike>tan</strike>"; 
String formatted = String.format(s,old,tan); //The cold hand reaches for the <font color="blue">old</font> <strike>tan</strike> Ellesse's

Spannable spannable = Html.fromHtml(formatted);

问题:这没有放入StrikethroughSpan。为了制作StrikethroughSpan,我们从this question 借用了一个自定义TagHandler

Spannable spannable = Html.fromHtml(text,null,new MyHtmlTagHandler());

MyTagHandler:

public class MyHtmlTagHandler implements Html.TagHandler 
    public void handleTag(boolean opening, String tag, Editable output,
                          XMLReader xmlReader) 
        if (tag.equalsIgnoreCase("strike") || tag.equals("s")) 
            processStrike(opening, output);
        
    
    private void processStrike(boolean opening, Editable output) 
        int len = output.length();
        if (opening) 
            output.setSpan(new StrikethroughSpan(), len, len, Spannable.SPAN_MARK_MARK);
         else 
            Object obj = getLast(output, StrikethroughSpan.class);
            int where = output.getSpanStart(obj);
            output.removeSpan(obj);
            if (where != len) 
                output.setSpan(new StrikethroughSpan(), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            
        
    

    private Object getLast(Editable text, Class kind) 
        Object[] objs = text.getSpans(0, text.length(), kind);
        if (objs.length == 0) 
            return null;
         else 
            for (int i = objs.length; i > 0; i--) 
                if (text.getSpanFlags(objs[i - 1]) == Spannable.SPAN_MARK_MARK) 
                    return objs[i - 1];
                
            
            return null;
        
    

【讨论】:

这行得通,但我认为它相比之下效率低下。【参考方案2】:

我创建了a version of String.format,它可以与 spannables 一起使用。下载它并像普通版本一样使用它。在您的情况下,您可以将跨度放在格式说明符周围(可能使用 strings.xml)。在输出中,它们将围绕那些被替换的说明符。

【讨论】:

我只用索引 1 (%1$s) 尝试了一个参数,就像在 android 开发人员的示例中一样,但是我的应用程序崩溃了,所以我将它更改为 0 并且它工作。所以有点矛盾。 developer.android.com/guide/topics/resources/… @SnapDragon 我刚刚推送了一个带有修复的新提交。感谢您发现错误。【参考方案3】:

我制作了简单的 Kotlin 扩展函数,应该可以解决 String.format 跨度问题:

fun Context.getStringSpanned(@StringRes resId: Int, vararg formatArgs: Any): Spanned 
    var lastArgIndex = 0
    val spannableStringBuilder = SpannableStringBuilder(getString(resId, *formatArgs))
    for (arg in formatArgs) 
        val argString = arg.toString()
        lastArgIndex = spannableStringBuilder.indexOf(argString, lastArgIndex)
        if (lastArgIndex != -1) 
            (arg as? CharSequence)?.let 
                spannableStringBuilder.replace(lastArgIndex, lastArgIndex + argString.length, it)
            
            lastArgIndex += argString.length
        
    

    return spannableStringBuilder

【讨论】:

不确定此解决方案是否正确,因为如果您的参数中有多个跨度,例如颜色不同但文本相同,那么它们的顺序可能不正确,因为您正在寻找文本内容替换为 spannable。此外,如果在字符串中您有一个与可跨文本的内容相同的子字符串,则可能会出现另一种极端情况,那么您的函数将替换在错误的位置。请参阅下面我的解决方案之一,以了解我认为的更好方向。【参考方案4】:

我遇到了同样的问题,但在这里找到了一个非常好的解决方案: Android: How to combine Spannable.setSpan with String.format?

查看@george-steel 提供的解决方案。他创建了一个custom version of String.format 来保留跨度。

使用示例:

Spanned toDisplay = SpanFormatter.format(getText(R.string.foo), bar, baz, quux);

【讨论】:

【参考方案5】:

我需要用一组 Spannables 替换字符串中的 %s 占位符,但没有找到任何令人满意的东西,所以我将自己的格式化程序实现为 kotlin 字符串扩展。希望它可以帮助任何人。

fun String.formatSpannable(vararg spans: CharSequence?): Spannable 
    val result = SpannableStringBuilder()
    when 
        spans.size != this.split("%s").size - 1 ->
            Log.e("formatSpannable",
                    "cannot format '$this' with $spans.size arguments")
        !this.contains("%s") -> result.append(this)
        else -> 
            var str = this
            var spanIndex = 0
            while (str.contains("%s")) 
                val preStr = str.substring(0, str.indexOf("%s"))
                result.append(preStr)
                result.append(spans[spanIndex] ?: "")
                str = str.substring(str.indexOf("%s") + 2)
                spanIndex++
            
            if (str.isNotEmpty()) 
                result.append(str)
            
        
    
    return result

然后用法如下

"hello %s kotlin %s world".formatSpannable(span0, span1)

【讨论】:

以上是关于将 Spannable 与 String.format() 结合使用的主要内容,如果未能解决你的问题,请参考以下文章

如何摆脱带有可点击对象的Spannable字符串中的下划线?

有没有关于 Spanned 和 Spannable 文本的例子

自定义 Spannable = ImageSpan + BackgroundColorSpan + ClickableSpan

什么是Objective-C中Android的“Spannable”等价物

Android 自定义 View 中使用 Spannable

Android 自定义 View 中使用 Spannable