Подтвердить что ты не робот

Как использовать форматированные строки вместе с заполнителями в Android?

В 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);

Также можно использовать некоторые HTML-теги в строковых ресурсах Android:

<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 целым числом?

4b9b3361

Ответ 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 вручную ни в форматируемой строке, ни в строках, которые заменяют заполнители.

Ответ 2

<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

Ответ 3

Для простого случая, когда вы хотите заменить заполнитель без форматирования номера (т.е. начальные нули, числа после запятой), вы можете использовать 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().

Ответ 4

Подобно принятому ответу, я попытался написать для этого метод расширения 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))
}

Проблема с принятым ответом заключается в том, что он не работает, когда стилизуются сами аргументы формата (т.е. Spanned, а не String). Экспериментально это кажется странным, возможно, связано с тем, что мы не избегаем не-String CharSequence. Я вижу, что если я позвоню

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

где R.id.my_format_string:

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

и myHelloSpanned - это Spanned, который выглядит как <b> привет </b> (т.е. он будет иметь HTML <i>&lt;b&gt;hello&lt;/b&gt;</i>), тогда я получаю === hello === ( то есть HTML ===<b>hello</b>===).

Это неправильно, я должен получить === <b> привет </b> ===.

Я попытался исправить это, преобразовав все CharSequence в HTML перед применением String.format, и вот мой результирующий код.

@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 он помещает теги <p> вокруг всего, даже если этого дополнительного отступа не было на входе. Html.fromHtml(Html.toHtml(myHelloSpanned)) словами, Html.fromHtml(Html.toHtml(myHelloSpanned)) не равен myHelloSpanned - он получил дополнительное заполнение. Я не знал, как решить это красиво.

Ответ 5

Функция расширения Kotlin, которая

  • работает со всеми версиями API
  • обрабатывает несколько аргументов

Пример использования

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

Ресурс строки HTML, помещенный в раздел CDATA

<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 {
  val text = String.format(getString(id), *args)
  return if (android.os.Build.VERSION.SDK_INT >= 24)
    Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT)
  else
    Html.fromHtml(text)
}