且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

Android Compose:如何在文本视图中使用 HTML 标签

更新时间:2022-12-26 18:13:27

由于我将 Kotlin Multiplatform 项目与 Android Jetpack Compose 和 JetBrains Compose for Desktop 结合使用,因此我真的没有选择只使用 Android 的文本视图.

所以我从 turbohenoch 的答案 中汲取了灵感,并尽力扩展它以能够解释多个(可能是嵌套的)) HTML 格式标签.

代码肯定可以改进,它对 HTML 错误一点也不健壮,但我确实用包含 <u><b> 的文本对其进行了测试code> 标签,它至少可以正常工作.

代码如下:

/*** 要解释的标签.在此处和 [tagToStyle] 中添加标签.*/私有 val 标签 =linkedMapOf(<b>"到</b>",<i>"到</i>",<u>"到</u>")/*** 主要入口点.在字符串上调用它并在文本中使用结果.*/有趣的 String.parseHtml(): AnnotatedString {val newlineReplace = this.replace("
", "\n")返回 buildAnnotatedString {递归(换行替换,这个)}}/*** 通过给定的 HTML 字符串递归将其转换为 AnnotatedString.** @param string 要检查的字符串.* @param 到要附加到的 AnnotatedString.*/私人乐趣递归(字符串:字符串,到:AnnotatedString.Builder){//查找给定字符串开头的开始标记,如果有的话.val startTag = tags.keys.find { string.startsWith(it) }//查找给定字符串开头的结束标记(如果有).val endTag = tags.values.find { string.startsWith(it) }什么时候 {//如果字符串以结束标记开头,则弹出最新应用的//SpanStyle 并继续递归.tags.any { string.startsWith(it.value) } ->{到.pop()递归(string.removeRange(0, endTag!!.length), to)}//如果字符串以开始标记开头,则应用适当的//SpanStyle 并继续递归.tags.any { string.startsWith(it.key) } ->{to.pushStyle(tagToStyle(startTag!!))递归(string.removeRange(0,startTag.length),到)}//如果字符串不以开始或结束标记开头,但确实包含其中之一,//找到开始或结束标签的最低索引(不是-1/未找到).//将文本正常追加到最低索引,然后从该索引开始递归.tags.any { string.contains(it.key) ||string.contains(it.value) } ->{val firstStart = tags.keys.map { string.indexOf(it) }.filterNot { it == -1 }.minOrNull() ?: -1val firstEnd = tags.values.map { string.indexOf(it) }.filterNot { it == -1 }.minOrNull() ?: -1val 第一个 = 当 {firstStart == -1 ->首尾firstEnd == -1 ->第一次开始否则 ->分钟(第一个开始,第一个结束)}to.append(string.substring(0, first))递归(string.removeRange(0,第一个),到)}//没有在文本中找到任何支持的标签.只需正常追加即可.否则 ->{to.append(字符串)}}}/*** 获取给定(开始)标签的 [SpanStyle].* 通过将其开始标签添加到此处添加您自己的标签样式* when 子句然后实例化适当的 [SpanStyle].** @return 给定标签的 [SpanStyle].*/私人乐趣 tagToStyle(tag: String): SpanStyle {返回时(标签){<b>"->{SpanStyle(fontWeight = FontWeight.Bold)}<i>"->{SpanStyle(fontStyle = FontStyle.Italic)}<u>"->{SpanStyle(textDecoration = TextDecoration.Underline)}//如果你在[tags] Map中添加了一个标签并且忘记添加它,这应该只会抛出//到这个函数.否则 ->throw IllegalArgumentException(标签 $tag 无效.")}}

我已尽力做出明确的评论,但这里有一个简短的解释.tags 变量是要跟踪的标签的映射,键是开始标签,值是它们对应的结束标签.这里的任何东西都需要在 tagToStyle() 函数中处理,这样代码才能为每个标签获得合适的 SpanStyle.

然后递归扫描输入字符串,寻找跟踪的开始和结束标签.

如果给定的 String 以结束标记开头,它将弹出最近应用的 SpanStyle(从那时起将其从附加的文本中删除)并在删除该标记的 String 上调用递归函数.

如果给定的 String 以开始标签开头,它将推送相应的 SpanStyle(使用 tagToStyle()),然后在移除该标签的 String 上调用递归函数.>

如果给定的字符串不以结束标签或开始标签开头,但确实至少包含其中之一,它将找到任何跟踪标签的第一次出现(开始或关闭),通常将给定字符串中的所有文本附加到该索引,然后在字符串上调用递归函数,从它找到的第一个跟踪标签的索引开始.

如果给定的字符串没有任何标签,它只会正常追加,不会添加或删除任何样式.

由于我在一个正在积极开发的应用程序中使用它,我可能会根据需要继续更新它.假设没有太大的变化,最新版本应该可以在它的 GitHub 存储库.

I have as string from an outside source that contains HTML tags in this format: "Hello, I am <b> bold</b> text"

Before Compose I would have CDATA at the start of my HTML String, used Html.fromHtml() to convert to a Spanned and passed it to the TextView. The TextView would them have the word bold in bold.

I have tried to replicated this with Compose but I can't find the exact steps to allow me to achieve it successfully.

Any suggestions are gratefully received.

Since I'm using a Kotlin Multiplatform project with Android Jetpack Compose and JetBrains Compose for Desktop, I don't really have the option of just falling back to Android's TextView.

So I took inspiration from turbohenoch's answer and did my best to expand it to be able to interpret multiple (possibly nested) HTML formatting tags.

The code can definitely be improved, and it's not at all robust to HTML errors, but I did test it with text that contained <u> and <b> tags and it works fine for that at least.

Here's the code:

/**
 * 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.")
    }
}

I did my best to make clear comments, but here's a quick explanation. The tags variable is a map of the tags to track, with the keys being the opening tags and the values being their corresponding closing tags. Anything here needs to also be handled in the tagToStyle() function, so that the code can get a proper SpanStyle for each tag.

It then recursively scans the input String, looking for tracked opening and closing tags.

If the String it's given starts with a closing tag, it'll pop the most recently-applied SpanStyle (removing it from text appended from then on) and call the recursive function on the String with that tag removed.

If the String it's given starts with an opening tag, it'll push the corresponding SpanStyle (using tagToStyle()) and then call the recursive function on the String with that tag removed.

If the String it's given doesn't start with either a closing or opening tag, but does contain at least one of either, it'll find the first occurrence of any tracked tag (opening or closing), normally append all text in the given String up until that index, and then call the recursive function on the String starting at the index of the first tracked tag it finds.

If the String it's given doesn't have any tags, it'll just append normally, without adding or removing any styling.

Since I'm using this in an app being actively developed, I'll probably continue to update it as needed. Assuming nothing drastic changes, the latest version should be available on its GitHub repository.