Как использовать форматированные строки вместе с заполнителями в 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
целым числом?
Ответы
Ответ 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 <b>%2$d new messages</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><b>hello</b></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)
}