Reputation: 941
I have a string in my strings.xml
which is localized for different languages.
The strings are styled with Html tags for each localization.
Using Android TextView
, I was able to show the styled text just fine by reading the string resources.
Considering that Jetpack Compose currently (1.0.0-rc02)
does not support Html tags, I tried using TextView
inside an AndroidView
composable following Official Docs: https://developer.android.com/jetpack/compose/interop/interop-apis#views-in-compose
Example of what I tried:
@Composable
fun StyledText(text: String, modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier,
factory = { context -> TextView(context) },
update = {
it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
)
}
The text in strings.xml
file:
<string name="styled_text">Sample text with <b>bold styling</b> to test</string>
However, using stringResource(id = R.string.styled_text)
provides the text without the Html tags.
Is there a way to show text from string resources with Html styles in Jetpack Compose?
The following two questions are similar, but they do not read the string from resources:
Jetpack compose display html in text
Android Compose: How to use HTML tags in a Text view
Upvotes: 23
Views: 20654
Reputation: 88162
stringResource
under the hood uses resources.getString
, which discards any styled information.
Since 1.7.0 there's a new method to parse html to AnnotatedString
, AnnotatedString.fromHtml
, here's how you can use it to read styled resource string:
@Composable
@ReadOnlyComposable
fun annotatedStringResource(
@StringRes id: Int,
): AnnotatedString {
val text = LocalContext.current.resources.getText(id)
val html = if (text is Spanned) {
text.toHtml(TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
} else {
text.toString()
}
return AnnotatedString.fromHtml(html)
}
@Composable
@ReadOnlyComposable
fun annotatedStringResource(
@StringRes id: Int,
vararg formatArgs: Any,
): AnnotatedString {
val text = LocalContext.current.resources.getText(id)
val html = if (text is Spanned) {
text.toHtml(TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
} else {
text.toString()
}
val encodedArgs = formatArgs.map { if (it is String) it.htmlEncode() else it }.toTypedArray()
return AnnotatedString.fromHtml(html.format(*encodedArgs))
}
Note that it also has linkStyles
and linkInteractionListener
parameters that you may want to use to customize the result
Alternatively, you can use TextView
that will handle it for you
@Composable
@ReadOnlyComposable
fun textResource(@StringRes id: Int): CharSequence =
LocalContext.current.resources.getText(id)
And use it like this:
StyledText(textResource(id = R.string.foo))
@Composable
fun StyledText(text: CharSequence, modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier,
factory = { context -> TextView(context) },
update = {
it.text = text
}
)
}
Upvotes: 15
Reputation: 21
As others have said, stringResource(id = R.string.styled_text)
will remove HTML tags. To prevent this, you can escape the angle bracket symbols using <
for the "less than" sign (<) and >
for the "greater than" sign (>). Your string resource would then be:
<string name="styled_text">Sample text with <b>bold styling</b> to test</string>
Upvotes: 0
Reputation: 304
To keep using stringResources()
you can do as down below.
1st - Use <![CDATA[ … ]]>
Embrace all your resource HTML strings between <![CDATA[ … ]]>
then you are not going to lose the HTML definitions when calling stringResources()
:
<string name="styled_text"><![CDATA[Sample text with <b>bold styling</b> to test]]></string>
2nd - Create a "from Spanned
to AnnotatedString
processor"
First of all thanks to this awesome answer.
Then it is important to know that the text
param (from Text()
composable) does also accept an AnnotatedString
object and not only a String
.
Moving forward… we can create a "from Spanned
to AnnotatedString
processor" algorithm using the Jetpack buildAnnotatedString()
.
In this case I would pick the "create the algorithm as an extension private
function inside of the client composable file" path:
private fun Spanned.toAnnotatedString(): AnnotatedString =
buildAnnotatedString {
val spanned = this@toAnnotatedString
append(spanned.toString())
getSpans(
0,
spanned.length,
Any::class.java
).forEach { span ->
val start = getSpanStart(span)
val end = getSpanEnd(span)
when (span) {
is StyleSpan ->
when (span.style) {
Typeface.BOLD -> addStyle(
SpanStyle(fontWeight = FontWeight.Bold),
start,
end
)
Typeface.ITALIC -> addStyle(
SpanStyle(fontStyle = FontStyle.Italic),
start,
end
)
Typeface.BOLD_ITALIC -> addStyle(
SpanStyle(
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Italic
),
start,
end
)
}
is ForegroundColorSpan -> addStyle( // For <span style=color:blue> tag.
SpanStyle(color = Color.Blue),
start,
end
)
}
}
}
3rd - Call HtmlCompat.fromHtml()
As a last step before sending the AnnotatedString
to the Text()
composable we need to call the target String
on HtmlCompat.fromHtml()
method and also the new toAnnotatedString()
extension function:
val textAsAnnotatedString = HtmlCompat.fromHtml(
stringResource(id = R.string.styled_text),
HtmlCompat.FROM_HTML_MODE_COMPACT
).toAnnotatedString()
4th - Show it on Text()
And then just show it on your target Text()
composable:
Text(text = textAsAnnotatedString)
Note: you can add lot's of "style interpreter" inside of toAnnotatedString()
.
The down below print (everything inside of the red rectangle) is from a composable SnackBar
on my Android project using the same above strategy.
Upvotes: 4
Reputation: 754
There is ongoing discussion to implement on Jetpack Compose UI: https://issuetracker.google.com/issues/139320238
After some research, I came me up with the following solution that I also posted in the same discussion:
@Composable
@ReadOnlyComposable
private fun resources(): Resources {
LocalConfiguration.current
return LocalContext.current.resources
}
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)
}
@Composable
fun annotatedStringResource(@StringRes id: Int, vararg formatArgs: Any): AnnotatedString {
val resources = resources()
val density = LocalDensity.current
return remember(id, formatArgs) {
val text = resources.getText(id, *formatArgs)
spannableStringToAnnotatedString(text, density)
}
}
@Composable
fun annotatedStringResource(@StringRes id: Int): AnnotatedString {
val resources = resources()
val density = LocalDensity.current
return remember(id) {
val text = resources.getText(id)
spannableStringToAnnotatedString(text, density)
}
}
private fun spannableStringToAnnotatedString(
text: CharSequence,
density: Density
): AnnotatedString {
return if (text is Spanned) {
with(density) {
buildAnnotatedString {
append((text.toString()))
text.getSpans(0, text.length, Any::class.java).forEach {
val start = text.getSpanStart(it)
val end = text.getSpanEnd(it)
when (it) {
is StyleSpan -> when (it.style) {
Typeface.NORMAL -> addStyle(
SpanStyle(
fontWeight = FontWeight.Normal,
fontStyle = FontStyle.Normal
),
start,
end
)
Typeface.BOLD -> addStyle(
SpanStyle(
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Normal
),
start,
end
)
Typeface.ITALIC -> addStyle(
SpanStyle(
fontWeight = FontWeight.Normal,
fontStyle = FontStyle.Italic
),
start,
end
)
Typeface.BOLD_ITALIC -> addStyle(
SpanStyle(
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Italic
),
start,
end
)
}
is TypefaceSpan -> addStyle(
SpanStyle(
fontFamily = when (it.family) {
FontFamily.SansSerif.name -> FontFamily.SansSerif
FontFamily.Serif.name -> FontFamily.Serif
FontFamily.Monospace.name -> FontFamily.Monospace
FontFamily.Cursive.name -> FontFamily.Cursive
else -> FontFamily.Default
}
),
start,
end
)
is BulletSpan -> {
Log.d("StringResources", "BulletSpan not supported yet")
addStyle(SpanStyle(), start, end)
}
is AbsoluteSizeSpan -> addStyle(
SpanStyle(fontSize = if (it.dip) it.size.dp.toSp() else it.size.toSp()),
start,
end
)
is RelativeSizeSpan -> addStyle(
SpanStyle(fontSize = it.sizeChange.em),
start,
end
)
is StrikethroughSpan -> addStyle(
SpanStyle(textDecoration = TextDecoration.LineThrough),
start,
end
)
is UnderlineSpan -> addStyle(
SpanStyle(textDecoration = TextDecoration.Underline),
start,
end
)
is SuperscriptSpan -> addStyle(
SpanStyle(baselineShift = BaselineShift.Superscript),
start,
end
)
is SubscriptSpan -> addStyle(
SpanStyle(baselineShift = BaselineShift.Subscript),
start,
end
)
is ForegroundColorSpan -> addStyle(
SpanStyle(color = Color(it.foregroundColor)),
start,
end
)
else -> addStyle(SpanStyle(), start, end)
}
}
}
}
} else {
AnnotatedString(text.toString())
}
}
Source: https://issuetracker.google.com/issues/139320238#comment11
With these helper methods, you can simply call:
Text(annotatedStringResource(R.string.your_string_resource))
Upvotes: 16
Reputation: 11
Currently, you can use AnnotatedString
class in order to show styled texts within a Text
component. I will show you an example:
Text(
text = buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Thin)) {
append(stringResource(id = R.string.thin_string))
}
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(stringResource(id = R.string.bold_string))
}
},
)
Depending on the style you assign, you will get a text with the style of the red highlight
https://i.sstatic.net/XSMMB.png
For more info: https://developer.android.com/jetpack/compose/text?hl=nl#multiple-styles
Upvotes: 0