【问题标题】:Combining Spannable with String.format()将 Spannable 与 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 几乎可以肯定。

标签: android spannablestring spannable


【解决方案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)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-11-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-16
      • 2019-08-15
      • 2018-10-03
      相关资源
      最近更新 更多