如何在 Android 中将格式化字符串与占位符一起使用?

Posted

技术标签:

【中文标题】如何在 Android 中将格式化字符串与占位符一起使用?【英文标题】:How to use formatted strings together with placeholders in Android? 【发布时间】:2014-06-23 14:16:22 【问题描述】:

android 中可以在字符串中使用占位符,例如:

<string name="number">My number is %1$d</string>

然后在 Java 代码中(在Activity 的子类中):

String res = getString(R.string.number);
String formatted = String.format(res, 5);

甚至更简单:

String formatted = getString(R.string.number, 5);

在Android字符串资源中也可以使用一些html标签:

<string name="underline"><u>Underline</u> example</string>

由于String 本身不能保存任何有关格式化的信息,因此应该使用getText(int) 而不是getString(int) 方法:

CharSequence formatted = getText(R.string.underline);

然后可以将返回的CharSequence 传递给Android 小部件,例如TextView,标记的短语将带有下划线。

但是,我找不到如何结合这两种方法,使用格式化字符串和占位符:

<string name="underlined_number">My number is <u>%1$d</u></string>

如何在Java代码中处理上述资源以将其显示在TextView中,将%1$d替换为整数?

【问题讨论】:

【参考方案1】:

最后我设法找到了一个可行的解决方案并编写了我自己的方法来替换占位符,保留格式:

public static CharSequence getText(Context context, int id, Object... args) 
    for(int i = 0; i < args.length; ++i)
        args[i] = args[i] instanceof String? TextUtils.htmlEncode((String)args[i]) : args[i];
    return Html.fromHtml(String.format(Html.toHtml(new SpannedString(context.getText(id))), args));

这种方法不需要手动转义 HTML 标记,无论是在格式化的字符串中还是在替换占位符的字符串中。

【讨论】:

您还应该删除尾随\n:private static CharSequence removeTrailingLineFeed(@NonNull CharSequence text) while (text.charAt(text.length() - 1) == '\n') text = text.subSequence(0, text.length() - 1); return text; @fernandospr 确保在 while() 条件中检查 length() > 0。 如今,Kotlin 扩展可能更有意义。 我这样做了,它在下面添加了一些边距或空换行符之王。有人遇到同样的问题吗?【参考方案2】:

Kotlin 扩展函数

适用于所有 API 版本 处理多个参数

示例用法

textView.text = context.getText(R.string.html_formatted, "Hello in bold")

封装在 CDATA 部分中的 HTML 字符串资源

<string name="html_formatted"><![CDATA[ bold text: <B>%1$s</B>]]></string>

结果

粗体文本:你好,粗体

代码

/**
* Create a formatted CharSequence from a string resource containing arguments and HTML formatting
*
* The string resource must be wrapped in a CDATA section so that the HTML formatting is conserved.
*
* Example of an HTML formatted string resource:
* <string name="html_formatted"><![CDATA[ bold text: <B>%1$s</B> ]]></string>
*/
fun Context.getText(@StringRes id: Int, vararg args: Any?): CharSequence =
    HtmlCompat.fromHtml(String.format(getString(id), *args), HtmlCompat.FROM_HTML_MODE_COMPACT)

【讨论】:

这应该是正确的答案。由于不推荐使用 Html.fromHtml,因此应将 Html 更新为 HtmlCompat。我认为这仍然是最好的答案 可以简化为getString(id).format(*args).parseAsHtml() 不幸的是,它对我不起作用。接受的答案虽然有效。【参考方案3】:
<resources>
  <string name="welcome_messages">Hello, %1$s! You have &lt;b>%2$d new messages&lt;/b>.</string>
</resources>


Resources res = getResources();
String text = String.format(res.getString(R.string.welcome_messages), username, mailCount);
CharSequence styledText = Html.fromHtml(text);

更多信息在这里:http://developer.android.com/guide/topics/resources/string-resource.html

【讨论】:

我明白了,这个页面上有描述。然而,这似乎是一个非常丑陋的解决方案,因为需要在原始字符串以及传递给 String.format 方法的字符串中转义 HTML 标记。 我猜 getText() 在后台做同样的事情?不知道有什么其他的想法。【参考方案4】:

对于要替换没有数字格式的占位符(即前导零、逗号后的数字)的简单情况,您可以使用 Square Phrase 库。

用法很简单:首先你要把你的字符串资源中的占位符改成这种更简单的格式:

<string name="underlined_number">My number is <u> number </u></string>

那么你可以像这样进行替换:

CharSequence formatted = Phrase.from(getResources(), R.string.underlined_number)
   .put("number", 5)
   .format()

格式化的CharSequence 也有样式。如果您需要格式化您的数字,您始终可以使用 String.format("%03d", 5) 对其进行预格式化,然后在 .put() 函数中使用生成的字符串。

【讨论】:

【参考方案5】:

与接受的答案类似,我尝试为此编写 Kotlin 扩展方法。

这是 Kotlin 中公认的答案

@Suppress("DEPRECATION")
fun Context.getText(id: Int, vararg args: Any): CharSequence 
    val escapedArgs = args.map 
        if (it is String) TextUtils.htmlEncode(it) else it
    .toTypedArray()
    return Html.fromHtml(String.format(Html.toHtml(SpannedString(getText(id))), *escapedArgs))

已接受答案的问题在于,当格式参数本身被设置样式(即跨区,而不是字符串)时,它似乎不起作用。通过实验,它似乎做了一些奇怪的事情,可能与我们没有转义非字符串 CharSequences 的事实有关。如果我打电话,我会看到

context.getText(R.id.my_format_string, myHelloSpanned)

其中 R.id.my_format_string 是:

<string name="my_format_string">===%1$s===</string>

myHelloSpanned 是一个看起来像 hello 的 Spanned(即它会有 HTML &lt;i&gt;&amp;lt;b&amp;gt;hello&amp;lt;/b&amp;gt;&lt;/i&gt;)然后我得到 ===hello ===(即 HTML ===&lt;b&gt;hello&lt;/b&gt;===)。

错了,我应该得到 ===hello===。

我尝试通过在应用 String.format 之前将所有 CharSequence 转换为 HTML 来解决此问题,这是我的结果代码。

@Suppress("DEPRECATION")
fun Context.getText(@StringRes resId: Int, vararg formatArgs: Any): CharSequence 
    // First, convert any styled Spanned back to HTML strings before applying String.format. This
    // converts the styling to HTML and also does HTML escaping.
    // For other CharSequences, just do HTML escaping.
    // (Leave any other args alone.)
    val htmlFormatArgs = formatArgs.map 
        if (it is Spanned) 
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) 
                Html.toHtml(it, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
             else 
                Html.toHtml(it)
            
         else if (it is CharSequence) 
            Html.escapeHtml(it)
         else 
            it
        
    .toTypedArray()

    // Next, get the format string, and do the same to that.
    val formatString = getText(resId);
    val htmlFormatString = if (formatString is Spanned) 
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) 
            Html.toHtml(formatString, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
         else 
            Html.toHtml(formatString)
        
     else 
        Html.escapeHtml(formatString)
    

    // Now apply the String.format
    val htmlResultString = String.format(htmlFormatString, *htmlFormatArgs)

    // Convert back to a CharSequence, recovering any of the HTML styling.
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) 
        Html.fromHtml(htmlResultString, Html.FROM_HTML_MODE_LEGACY)
     else 
        Html.fromHtml(htmlResultString)
    

但是,这并不太奏效,因为当您调用 Html.toHtml 时,它会将 &lt;p&gt; 标记放在所有内容周围,即使输入中没有额外的填充。换句话说,Html.fromHtml(Html.toHtml(myHelloSpanned)) 不等于 myHelloSpanned - 它有额外的填充。我不知道如何很好地解决这个问题。

【讨论】:

如果我理解你的答案正确,为了删除多余的填充,你可以使用.trim()函数来返回结果。【参考方案6】:

这是最终对我有用的代码

strings.xml

<string name="launch_awaiting_instructions">Contact <b>our</b> team on %1$s to activate.</string>
<string name="support_contact_phone_number"><b>555 555 555</b> Opt <b>3</b></string>

Kotlin 代码

fun Spanned.toHtmlWithoutParagraphs(): String 
    return HtmlCompat.toHtml(this, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
        .substringAfter("<p dir=\"ltr\">").substringBeforeLast("</p>")


fun Resources.getText(@StringRes id: Int, vararg args: Any): CharSequence 
    val escapedArgs = args.map 
        if (it is Spanned) it.toHtmlWithoutParagraphs() else it
    .toTypedArray()
    val resource = SpannedString(getText(id))
    val htmlResource = resource.toHtmlWithoutParagraphs()
    val formattedHtml = String.format(htmlResource, *escapedArgs)
    return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)

使用它,我也可以在带有样式占位符的 Android 上呈现样式文本

输出

联系我们的团队555 555 555选择3激活。

然后我能够扩展此解决方案以创建以下 Compose 方法。

Jetpack 组合用户界面

@Composable
fun annotatedStringResource(@StringRes id: Int, vararg formatArgs: Any): AnnotatedString 
    val resources = LocalContext.current.resources
    return remember(id) 
        val text = resources.getText(id, *formatArgs)
        spannableStringToAnnotatedString(text)
    


@Composable
fun annotatedStringResource(@StringRes id: Int): AnnotatedString 
    val resources = LocalContext.current.resources
    return remember(id) 
        val text = resources.getText(id)
        spannableStringToAnnotatedString(text)
    


private fun spannableStringToAnnotatedString(text: CharSequence): AnnotatedString 
    return if (text is Spanned) 
        val spanStyles = mutableListOf<AnnotatedString.Range<SpanStyle>>()
        spanStyles.addAll(text.getSpans(0, text.length, UnderlineSpan::class.java).map 
            AnnotatedString.Range(
                SpanStyle(textDecoration = TextDecoration.Underline),
                text.getSpanStart(it),
                text.getSpanEnd(it)
            )
        )
        spanStyles.addAll(text.getSpans(0, text.length, StyleSpan::class.java).map 
            AnnotatedString.Range(
                SpanStyle(fontWeight = FontWeight.Bold),
                text.getSpanStart(it),
                text.getSpanEnd(it)
            )
        )
        AnnotatedString(text.toString(), spanStyles = spanStyles)
     else 
        AnnotatedString(text.toString())
    

【讨论】:

【参考方案7】:

更新:此答案https://***.com/a/56944152/6007104 已更新,现在是首选答案

这是一个可读性更强的 Kotlin 扩展,它不使用已弃用的 API,适用于所有 Android 版本,并且不需要将字符串包装在 CDATA 部分中:

fun Context.getText(id: Int, vararg args: Any): CharSequence 

    val escapedArgs = args.map 
        if (it is String) TextUtils.htmlEncode(it) else it
    .toTypedArray()

    val resource = SpannedString(getText(id))
    val htmlResource = HtmlCompat.toHtml(resource, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
    val formattedHtml = String.format(htmlResource, *escapedArgs)
    return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)

您可以添加别名作为片段的扩展 - 只需记住将 args 分布在两者之间:

fun Fragment.getText(id: Int, vararg args: Any) = requireContext().getText(id, *args)

【讨论】:

我这样做了,它在下面添加了一些边距之王或空换行符。有人遇到同样的问题吗? 也许您可以使用不同的 HTML 模式来获得您想要的?也许尝试调整TO_HTML_PARAGRAPH_LINES_CONSECUTIVEFROM_HTML_MODE_LEGACY。最坏的结果,你可能想添加一个最终的.trim() 我最终使用了一个最终的 trim(),它解决了这个问题。不知道为什么,但确实如此【参考方案8】:

您可以在 Kotlin 中使用 java.lang.String 进行字符串格式化

fun main(args : Array<String>) 
  var value1 = 1
  var value2 = "2"
  var value3 = 3.0
  println(java.lang.String.format("%d, %s, %6f", value1, value2, value3))

【讨论】:

以上是关于如何在 Android 中将格式化字符串与占位符一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

Android多语言支持:由于占位符计数不同导致的字符串格式问题

如何在 Android 中将字符串解析为日期? [复制]

如何在android中将EditText提示创建为带有图像的文本

如何在 Android 中将字符串转换为 UTF-8?

如何在 Android 中将颜色整数转换为十六进制字符串?

在Android中将字符串日期转换为时间戳?