我从外部源获得了一个包含以下格式HTML标签的字符串:
"Hello, I am <b> bold</b> text"
在使用Compose之前,我会在HTML字符串开头使用CDATA,并使用Html.fromHtml()将其转换为Spanned对象,然后将其传递给TextView。这样TextView中的单词"bold"将会以加粗字体呈现。
我已经尝试在Compose中复制此过程,但是我找不到确切的步骤来使我成功地实现它。
任何建议都将不胜感激。
我从外部源获得了一个包含以下格式HTML标签的字符串:
"Hello, I am <b> bold</b> text"
在使用Compose之前,我会在HTML字符串开头使用CDATA,并使用Html.fromHtml()将其转换为Spanned对象,然后将其传递给TextView。这样TextView中的单词"bold"将会以加粗字体呈现。
我已经尝试在Compose中复制此过程,但是我找不到确切的步骤来使我成功地实现它。
任何建议都将不胜感激。
目前还没有官方的 Composable 可以完成这个任务。现在我正在使用一个带有 TextView 的 AndroidView。虽然不是最好的解决方案,但它很简单并且可以解决问题。
@Composable
fun HtmlText(html: String, modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier,
factory = { context -> TextView(context) },
update = { it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) }
)
}
如果HTML中有标签,您需要设置TextView
属性movementMethod = LinkMovementMethod.getInstance()
使链接可点击。Span
(Spanned)转换为SpanStyle
(AnnotatedString/Compose)替代品。 /**
* Converts a [Spanned] into an [AnnotatedString] trying to keep as much formatting as possible.
*
* Currently supports `bold`, `italic`, `underline` and `color`.
*/
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 UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
is ForegroundColorSpan -> addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)
}
}
}
val spannableString = SpannableStringBuilder("<b>Hello</b> <i>World</i>").toString()
val spanned = HtmlCompat.fromHtml(spannableString, HtmlCompat.FROM_HTML_MODE_COMPACT)
Text(text = spanned.toAnnotatedString())
fontWeight
样式一起传递时。 - Eugen Martynov由于我正在使用带有Android Jetpack Compose和JetBrains Compose for Desktop的Kotlin Multiplatform项目,因此我没有回退到Android的TextView的选项。
因此,我受到了turbohenoch的答案的启发,并尽力扩展它以能够解释多个(可能嵌套的)HTML格式标记。
代码肯定可以改进,而且对HTML错误不太健壮,但是我确实测试了包含<u>
和<b>
标记的文本,并且至少对此有效。
以下是代码:
/**
* The tags to interpret. Add tags here and in [tagToStyle].
*/
private val tags = linkedMapOf(
"<b>" to "</b>",
"<i>" to "</i>",
"<u>" to "</u>"
)
/**
* The main entry point. Call this on a String and use the result in a Text.
*/
fun String.parseHtml(): AnnotatedString {
val newlineReplace = this.replace("<br>", "\n")
return buildAnnotatedString {
recurse(newlineReplace, this)
}
}
/**
* Recurses through the given HTML String to convert it to an AnnotatedString.
*
* @param string the String to examine.
* @param to the AnnotatedString to append to.
*/
private fun recurse(string: String, to: AnnotatedString.Builder) {
//Find the opening tag that the given String starts with, if any.
val startTag = tags.keys.find { string.startsWith(it) }
//Find the closing tag that the given String starts with, if any.
val endTag = tags.values.find { string.startsWith(it) }
when {
//If the String starts with a closing tag, then pop the latest-applied
//SpanStyle and continue recursing.
tags.any { string.startsWith(it.value) } -> {
to.pop()
recurse(string.removeRange(0, endTag!!.length), to)
}
//If the String starts with an opening tag, apply the appropriate
//SpanStyle and continue recursing.
tags.any { string.startsWith(it.key) } -> {
to.pushStyle(tagToStyle(startTag!!))
recurse(string.removeRange(0, startTag.length), to)
}
//If the String doesn't start with an opening or closing tag, but does contain either,
//find the lowest index (that isn't -1/not found) for either an opening or closing tag.
//Append the text normally up until that lowest index, and then recurse starting from that index.
tags.any { string.contains(it.key) || string.contains(it.value) } -> {
val firstStart = tags.keys.map { string.indexOf(it) }.filterNot { it == -1 }.minOrNull() ?: -1
val firstEnd = tags.values.map { string.indexOf(it) }.filterNot { it == -1 }.minOrNull() ?: -1
val first = when {
firstStart == -1 -> firstEnd
firstEnd == -1 -> firstStart
else -> min(firstStart, firstEnd)
}
to.append(string.substring(0, first))
recurse(string.removeRange(0, first), to)
}
//There weren't any supported tags found in the text. Just append it all normally.
else -> {
to.append(string)
}
}
}
/**
* Get a [SpanStyle] for a given (opening) tag.
* Add your own tag styling here by adding its opening tag to
* the when clause and then instantiating the appropriate [SpanStyle].
*
* @return a [SpanStyle] for the given tag.
*/
private fun tagToStyle(tag: String): SpanStyle {
return when (tag) {
"<b>" -> {
SpanStyle(fontWeight = FontWeight.Bold)
}
"<i>" -> {
SpanStyle(fontStyle = FontStyle.Italic)
}
"<u>" -> {
SpanStyle(textDecoration = TextDecoration.Underline)
}
//This should only throw if you add a tag to the [tags] Map and forget to add it
//to this function.
else -> throw IllegalArgumentException("Tag $tag is not valid.")
}
}
tags
变量是要跟踪的标签映射,键为起始标签,值为对应的结束标签。在tagToStyle()
函数中也需要处理这些内容,以便代码可以为每个标签获取适当的SpanStyle。tagToStyle()
),然后在删除该标记的情况下调用该字符串的递归函数。对于简单的使用情况,您可以像这样做:
private fun String.parseBold(): AnnotatedString {
val parts = this.split("<b>", "</b>")
return buildAnnotatedString {
var bold = false
for (part in parts) {
if (bold) {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(part)
}
} else {
append(part)
}
bold = !bold
}
}
}
然后在@Composable中使用这个带注释的字符串
Text(text = "Hello, I am <b> bold</b> text".parseBold())
<string name="intro"><![CDATA[Hello, I am <b> bold</b> text]]></string>
Text
布局不提供任何HTML支持,该库通过暴露可组合的HtmlText
布局来填补这一空白。它是基于Text
布局和Span/Spannable
Android类构建的(实现基于@Sven的答案)。其API如下所示:HtmlText(
text = htmlString,
linkClicked = { link ->
Log.d("linkClicked", link)
}
)
这些是所有可用的参数,允许您更改默认行为:
fun HtmlText(
text: String,
modifier: Modifier = Modifier,
style: TextStyle = TextStyle.Default,
softWrap: Boolean = true,
overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
linkClicked: (String) -> Unit = {},
fontSize: TextUnit = 14.sp,
flags: Int = HtmlCompat.FROM_HTML_MODE_COMPACT,
URLSpanStyle: SpanStyle = SpanStyle(
color = linkTextColor(),
textDecoration = TextDecoration.Underline
)
)
HtmlText
支持几乎与android.widget.TextView
相同数量的HTML标签,但不支持<img>
标签和<ul>
标签,后者部分支持,因为HtmlText
可以正确渲染列表元素,但不会添加项目符号(•)。
这是我的解决方案,还支持超链接:
@Composable
fun HtmlText(
html: String,
modifier: Modifier = Modifier,
style: TextStyle = TextStyle.Default,
hyperlinkStyle: TextStyle = TextStyle.Default,
softWrap: Boolean = true,
overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
onHyperlinkClick: (uri: String) -> Unit = {}
) {
val spanned = remember(html) {
HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY, null, null)
}
val annotatedText = remember(spanned, hyperlinkStyle) {
buildAnnotatedString {
append(spanned.toString())
spanned.getSpans(0, spanned.length, Any::class.java).forEach { span ->
val startIndex = spanned.getSpanStart(span)
val endIndex = spanned.getSpanEnd(span)
when (span) {
is StyleSpan -> {
span.toSpanStyle()?.let {
addStyle(style = it, start = startIndex, end = endIndex)
}
}
is UnderlineSpan -> {
addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start = startIndex, end = endIndex)
}
is URLSpan -> {
addStyle(style = hyperlinkStyle.toSpanStyle(), start = startIndex, end = endIndex)
addStringAnnotation(tag = Tag.Hyperlink.name, annotation = span.url, start = startIndex, end = endIndex)
}
}
}
}
}
ClickableText(
annotatedText,
modifier = modifier,
style = style,
softWrap = softWrap,
overflow = overflow,
maxLines = maxLines,
onTextLayout = onTextLayout
) {
annotatedText.getStringAnnotations(tag = Tag.Hyperlink.name, start = it, end = it).firstOrNull()?.let {
onHyperlinkClick(it.item)
}
}
}
private fun StyleSpan.toSpanStyle(): SpanStyle? {
return when (style) {
Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
Typeface.BOLD_ITALIC -> SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
else -> null
}
}
private enum class Tag {
Hyperlink
}
import android.graphics.Typeface
来避免编译错误。 - Arpit PatelCompose Text()目前还不支持HTML,因为它刚刚进入Beta版,所以也许将来会支持。
我们目前实现的解决方案(并不完美)是回退到旧的TextView控件,这是Compose允许你做的。
https://developer.android.com/jetpack/compose/interop#views-in-compose
参照使用HTML标记进行样式设置的指南,结合Sven的答案,我编写了这个函数,可以像内置的stringResource()
函数一样使用:
/**
* Load a styled string resource with formatting.
*
* @param id the resource identifier
* @param formatArgs the format arguments
* @return the string data associated with the resource
*/
@Composable
fun annotatedStringResource(@StringRes id: Int, vararg formatArgs: Any): AnnotatedString {
val text = stringResource(id, *formatArgs)
val spanned = remember(text) {
HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
return remember(spanned) {
buildAnnotatedString {
append(spanned.toString())
spanned.getSpans(0, spanned.length, Any::class.java).forEach { span ->
val start = spanned.getSpanStart(span)
val end = spanned.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 UnderlineSpan ->
addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
is ForegroundColorSpan ->
addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)
}
}
}
}
}
HtmlText
中的文本设置样式。color
和style
参数,这也是Text
提供的参数,并将它们翻译成了TextView
的参数。@Composable
fun HtmlText(
html: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
style: TextStyle = LocalTextStyle.current,
) {
val textColor = color
.takeOrElse { style.color }
.takeOrElse { LocalContentColor.current.copy(alpha = LocalContentAlpha.current) }
.toArgb()
val density = LocalDensity.current
val textSize = with(density) {
style.fontSize
.takeOrElse { LocalTextStyle.current.fontSize }
.toPx()
}
val lineHeight = with(density) {
style.lineHeight
.takeOrElse { LocalTextStyle.current.lineHeight }
.roundToPx()
}
val formattedText = remember(html) {
HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
AndroidView(
modifier = modifier,
factory = { context ->
AppCompatTextView(context).apply {
setTextColor(textColor)
// I haven't found out how to extract the typeface from style so I created my_font_family.xml and set it here
typeface = ResourcesCompat.getFont(context, R.font.my_font_family)
setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
if (style.lineHeight.isSp) {
this.lineHeight = lineHeight
} else {
// Line Height could not be set
}
},
update = { it.text = formattedText }
)
}
toArgb()
将Compose Color转换为Color Int(并删除辅助函数)
4.直接使用AppCompatTextView
设置行高(并删除帮助器函数) - Peter F转换为字符串:
Text(text = Html.fromHtml(htmlSource).toString())