ComoposeでのText装飾
An English version follows
例えば文字列の一部をBOLDにしたい
ComposeViewで実直に実装すると以下のように文字列を分割して目的の文字列と前後の文字列を分割しないといけなくてとにかくめんどくさい。
val annotatedString = buildAnnotatedString {
append("This is ")
// Apply bold style to "BOLD"
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append("BOLD")
}
append(" text")
}
BasicText(
text = annotatedString,
style = TextStyle(color = Color.Black)
)
AndroidViewだとStringリソースなどにタグを埋め込んでおけば、TextViewにパースした文字列(Spanned)とを渡すだけOK
## This is <b>BOLD</b> text
<string name="test_html_text">This is <b>BOLD</b> text</string>
&
val htmlString = context.getString(R.string.test_html_text)
textView.text = HtmlCompat.fromHtml(htmlString, HtmlCompat.FROM_HTML_MODE_COMPACT)
どうするか
以下のような文字列の中のHTMLタグを解析して、文字列を装飾するComposable関数を用意する
@Composable
fun HtmlText(
htmlText: String,
modifier: Modifier = Modifier,
textAlign: TextAlign? = null,
defaultStyle: TextStyle = LocalTextStyle.current,
) {
val hyperLinkTextColor = MaterialTheme.colorScheme.primary
val annotatedString = remember(htmlText) {
val parsedHtml = HtmlCompat.fromHtml(
htmlText.replace("\n", "<br>"),
HtmlCompat.FROM_HTML_MODE_COMPACT
)
return@remember buildAnnotatedString {
append(parsedHtml)
parsedHtml.getSpans(0, parsedHtml.length, CharacterStyle::class.java).forEach { span ->
when (span) {
// Apply styles for URL spans
is URLSpan -> {
addStyle(
style = SpanStyle(
color = hyperLinkTextColor,
textDecoration = TextDecoration.Underline
),
start = parsedHtml.getSpanStart(span),
end = parsedHtml.getSpanEnd(span)
)
addStringAnnotation(
tag = URL_TAG,
annotation = span.url,
start = parsedHtml.getSpanStart(span),
end = parsedHtml.getSpanEnd(span)
)
}
// Apply styles for bold and italic spans
is StyleSpan -> {
val style = when (span.style) {
android.graphics.Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
android.graphics.Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
android.graphics.Typeface.BOLD_ITALIC -> SpanStyle(
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Italic
)
else -> null
}
style?.let {
addStyle(
style = it,
start = parsedHtml.getSpanStart(span),
end = parsedHtml.getSpanEnd(span)
)
}
}
else -> Unit // NOP
}
}
}
}
val context = LocalContext.current
ClickableText(
text = annotatedString,
modifier = modifier,
style = defaultStyle.merge(
TextStyle(
color = MaterialTheme.colors.onSurface,
textAlign = textAlign
)
),
onClick = { offset ->
annotatedString.getStringAnnotations(tag = URL_TAG, start = offset, end = offset)
.firstOrNull()?.let {
val url = it.item
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
}
)
}
ここの処理は文字列の中のタグを抽出して実直にstyleを設定してる。
以下のように呼び出すだけ。
## This is <b>BOLD</b>, <i>ITALIC</i>, <a href="https://test.com">HYPER_LINK</a> and <b><i><a href="https://google.com">ALL</a></i></b> text
<string name="test_html_text">This is <b>BOLD</b>, <i>ITALIC</i>, <a href="https://test.com">HYPER_LINK</a> and <b><i><a href="https://google.com">ALL</a></i></b> text</string>
&
HtmlText(htmlText = stringResource(id = R.string.test_html_text))
↓
HtmlTextのサンプルの実装では、Bタグ、Iタグ、Aタグに対応している。
タグが重複して適用された場合でもOK。大体の場合でこれくらいで十分なのではないだろうか。
これでComposeViewでもAndroidViewのようにStringリソースにHTMLタグを埋め込むだけで文字列を装飾できるようになった。
Here is the blog post in English:
How to Bold Parts of a String
Implementing this directly in ComposeView can be cumbersome as it involves splitting the string and separately handling the target string and the surrounding text. The implementation would look like this:
val annotatedString = buildAnnotatedString {
append("This is ")
// Apply bold style to "BOLD"
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append("BOLD")
}
append(" text")
}
BasicText(
text = annotatedString,
style = TextStyle(color = Color.Black)
)
With AndroidView, you can simply embed tags in the String resources, and then pass the parsed string (Spanned) to TextView:
## This is <b>BOLD</b> text
<string name="test_html_text">This is <b>BOLD</b> text</string>
&
val htmlString = context.getString(R.string.test_html_text)
textView.text = HtmlCompat.fromHtml(htmlString, HtmlCompat.FROM_HTML_MODE_COMPACT)
What to Do
Prepare a Composable function that parses HTML tags within strings and decorates them as shown below:
@Composable
fun HtmlText(
htmlText: String,
modifier: Modifier = Modifier,
textAlign: TextAlign? = null,
defaultStyle: TextStyle = LocalTextStyle.current,
) {
val hyperLinkTextColor = MaterialTheme.colorScheme.primary
val annotatedString = remember(htmlText) {
val parsedHtml = HtmlCompat.fromHtml(
htmlText.replace("\n", "<br>"),
HtmlCompat.FROM_HTML_MODE_COMPACT
)
return@remember buildAnnotatedString {
append(parsedHtml)
parsedHtml.getSpans(0, parsedHtml.length, CharacterStyle::class.java).forEach { span ->
when (span) {
// Apply styles for URL spans
is URLSpan -> {
addStyle(
style = SpanStyle(
color = hyperLinkTextColor,
textDecoration = TextDecoration.Underline
),
start = parsedHtml.getSpanStart(span),
end = parsedHtml.getSpanEnd(span)
)
addStringAnnotation(
tag = URL_TAG,
annotation = span.url,
start = parsedHtml.getSpanStart(span),
end = parsedHtml.getSpanEnd(span)
)
}
// Apply styles for bold and italic spans
is StyleSpan -> {
val style = when (span.style) {
android.graphics.Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
android.graphics.Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
android.graphics.Typeface.BOLD_ITALIC -> SpanStyle(
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Italic
)
else -> null
}
style?.let {
addStyle(
style = it,
start = parsedHtml.getSpanStart(span),
end = parsedHtml.getSpanEnd(span)
)
}
}
else -> Unit // NOP
}
}
}
}
val context = LocalContext.current
ClickableText(
text = annotatedString,
modifier = modifier,
style = defaultStyle.merge(
TextStyle(
color = MaterialTheme.colors.onSurface,
textAlign = textAlign
)
),
onClick = { offset ->
annotatedString.getStringAnnotations(tag = URL_TAG, start = offset, end = offset)
.firstOrNull()?.let {
val url = it.item
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
}
)
}
This process involves extracting tags from strings and directly setting styles.
Simply call it like this:
## This is <b>BOLD</b>, <i>ITALIC</i>, <a href="https://test.com">HYPER_LINK</a> and <b><i><a href="https://google.com">ALL</a></i></b> text
<string name="test_html_text">This is <b>BOLD</b>, <i>ITALIC</i>, <a href="https://test.com">HYPER_LINK</a> and <b><i><a href="https://google.com">ALL</a></i></b> text</string>
&
HtmlText(htmlText = stringResource(id = R.string.test_html_text))
↓
The HtmlText sample implementation supports B, I, and A tags. It works even when tags are applied in combination. This approach should suffice for most cases. Now, like AndroidView, you can decorate strings in ComposeView by simply embedding HTML tags in String resources.
Discussion