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

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

我有来自外部源的字符串,其中包含以下格式的 HTML 标记: “你好,我是粗体文字

在撰写之前,我会在 HTML 字符串的开头使用 CDATA,使用 Html.fromHtml() 转换为 Spanned 并将其传递给 TextView。 TextView 会将粗体字加粗。

我曾尝试使用 Compose 复制此方法,但找不到让我成功实现它的确切步骤。

感谢收到任何建议。

解决方法

Compose Text() 尚不支持 HTML。它只是刚刚进入测试版,所以它可能会到达。

我们目前实施的解决方案(这并不完美)是依靠旧的 TextView 控件,Compose 允许您这样做。

https://developer.android.com/jetpack/compose/interop#views-in-compose

https://proandroiddev.com/jetpack-compose-interop-part-1-using-traditional-views-and-layouts-in-compose-with-androidview-b6f1b1c3eb1

,

目前还没有官方的 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) }
    )
}
,

由于我将 Kotlin Multiplatform 项目与 Android Jetpack Compose 和 JetBrains Compose for Desktop 结合使用,因此我真的无法选择只使用 Android 的 TextView。

因此,我从 turbohenoch's answer 中汲取了灵感,并尽力将其扩展为能够解释多个(可能是嵌套的)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。

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

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

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

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

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

由于我在一个正在积极开发的应用程序中使用它,我可能会根据需要继续更新它。假设没有任何重大变化,最新版本应该可以在其 GitHub repository 上获得。

,

对于简单的用例,您可以执行以下操作:

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 中使用这个 AnnotatedString

Text(text = "Hello,I am <b> bold</b> text".parseBold())

当然,当您尝试支持更多标签时,这会变得更加棘手。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?
Java在半透明框架/面板/组件上重新绘画。
Java“ Class.forName()”和“ Class.forName()。newInstance()”之间有什么区别?
在此环境中不提供编译器。也许是在JRE而不是JDK上运行?
Java用相同的方法在一个类中实现两个接口。哪种接口方法被覆盖?
Java 什么是Runtime.getRuntime()。totalMemory()和freeMemory()?
java.library.path中的java.lang.UnsatisfiedLinkError否*****。dll
JavaFX“位置是必需的。” 即使在同一包装中
Java 导入两个具有相同名称的类。怎么处理?
Java 是否应该在HttpServletResponse.getOutputStream()/。getWriter()上调用.close()?
Java RegEx元字符(。)和普通点?