将 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);
是否有可靠的方法来了解old
和tan
的起始索引?
请注意,仅搜索 '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”等价物