This is an old question, but I am posting a more general solution than the accepted answer.
Reference is made to the following string resources:
<string name="string_1"><b>Bolded</b> <abc>Not bolded</abc></string>
<string name="string_2"><font bgcolor="red">Background red</font> No background color.</string>
<string name="string_3">The <b>capital</b> of %1 is %2\n%2 is the capital of %1.</string>
Android stores string resource tags separately from strings. Tags will always be consumed when read into an app.
var s1 = getString(R.string.string_1)
var s2 = getText(R.string.string_1)
s1 placed in a TextView will show "Bolded Not bolded" while s2 in a TextView will show "Bolded Not bolded". The tag "<abc>" has no interpretation, so it is lost.
If the background color is known at compile time then we can do the following:
textView.text = getText(R.string.string_2)
will display:

Of some interest is that while the font tag is supported by the Android framework and the HTML translation class (Html .java), the Html.java implementation does not support the bgcolor
attribute, so the following
var s4 = "<font bgcolor=\"red\">Background red</font> No background color."
textView.text = HtmlCompat.fromHtml(s4, FROM_HTML_MODE_LEGACY)
will not display the background color.
If the formatting is indeterminate at compile time, then we must do a little more work. Replacing string arguments with spanned text using getString(string_id, varargs)
fails as the OP notes. What is an alternative?
One way is to read a string in with placeholders intact.
getString(R.string.string_3)
will produce the string "The capital of %1 is %2\n%2 is the capital of %1.". We could then search for "%1", "%2", etc. and make the replacements with spanned text. In this case, the placeholder identifiers could be any unique set of characters.
It may be better, however, to use getText(R.string.string_3)
which will interpret any HTML codes supported by the framework.
The following code shows hot to make substitutions of spanned text into string_3
. The spanned text that will be substituted simply has the first letter highlighted.
textView.text = SpanFormatter.getText(this, R.string.string_3, { Int -> getArg(Int) })
private fun getArg(argNum: Int) =
when (argNum) {
1 -> { // Get the country with a highlighted first character.
SpannableString("France").apply {
setSpan(
BackgroundColorSpan(0x55FF0000),
0,
1,
SpannedString.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
2 -> { // Get the capital city with a highlighted first character.
SpannableString("Paris").apply {
setSpan(
BackgroundColorSpan(0x550000FF),
0,
1,
SpannedString.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
else -> throw IllegalArgumentException("$argNum is a bad argument number.")
}
SpanFormatter.kt
object SpanFormatter {
private const val END_OF_STRING = -1
private const val SPAN_FLAGS = SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
fun getText(
context: Context,
@StringRes stringId: Int,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
) = getText(context.getText(stringId), argFactory, argStartChar)
fun getText(
cs: CharSequence,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
): CharSequence {
// Mark all areas of substitution with an ArgNum span.
val sb = SpannableStringBuilder(cs)
var pos = sb.indexOf(argStartChar, 0)
while (pos != END_OF_STRING) {
var argEnd = pos + 1
while (argEnd < sb.length && sb[argEnd].isDigit()) ++argEnd
if (argEnd - pos > 1) {
val argnum = sb.substring(pos + 1, argEnd).toInt()
check(argnum > 0) {
"Incorrect argument number (%d) which must greater than zero.\nString: %s".format(
argnum
)
}
sb.setSpan(ArgMark(argnum), pos, argEnd, SPAN_FLAGS)
}
pos = sb.indexOf(argStartChar, argEnd)
}
// Replace all ArgMark spans with the appropriate substitution text.
val argMarkSpans = sb.getSpans<ArgMark>(0, sb.length)
argMarkSpans.forEach { argMarkSpan ->
val start = sb.getSpanStart(argMarkSpan)
val end = sb.getSpanEnd(argMarkSpan)
sb.replace(start, end, argFactory(argMarkSpan.argNum))
sb.removeSpan(argMarkSpan)
}
return sb
}
private data class ArgMark(val argNum: Int)
}
The foregoing displays:

And a simpler way without the use of the marking spans which aren't really needed:
SpanFormatter.kt
object SpanFormatter {
private const val END_OF_STRING = -1
fun getText(
context: Context,
@StringRes stringId: Int,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
) = getText(context.getText(stringId), argFactory, argStartChar)
fun getText(
cs: CharSequence,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
): CharSequence {
val sb = SpannableStringBuilder(cs)
var argStart = sb.indexOf(argStartChar, 0)
while (argStart != END_OF_STRING) {
var argEnd = argStart + 1
while (argEnd < sb.length && sb[argEnd].isDigit()) ++argEnd
if (argEnd - argStart > 1) {
val argNum = sb.substring(argStart + 1, argEnd).toInt()
argFactory(argNum).apply {
sb.replace(argStart, argEnd, this)
argEnd = argStart + length
}
}
argStart = sb.indexOf(argStartChar, argEnd)
}
return sb
}
}