Android 自定义解析html标签

用来解析类如下面代码里 html 标签样式.

html 复制代码
<span style="color: rgb(64, 169, 255);">文字内容</span>
<span style="color: rgb(64, 169, 255); font-size: 16px;"><strong>文字</strong></span>

使用:

Kotlin 复制代码
             val str = htmlStr.replace("span", CustomTagHandler.HANDLE_TAG)
             tvHtml.text =
                        HtmlCompat.fromHtml(
                        str,
                        HtmlCompat.FROM_HTML_MODE_COMPACT,
                        null,
                        CustomTagHandler()
                    )

CustomTagHandler.kt 文件:

Kotlin 复制代码
import android.graphics.Color
import android.text.Editable
import android.text.Html.TagHandler
import android.text.Spanned
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import org.xml.sax.XMLReader

/**
 * 自定义解析 html tag.
 *
 * 解析字符串样式:
 * 
 *     val str =
 *         """<p>
 *                <span style="color: rgb(64, 169, 255);">文字内容</span>
 *            </p>
 *            <p>
 *                <span style="color: rgb(64, 169, 255); font-size: 16px;"><strong>文字</strong></span>
 *            </p>
 *         """
 *
 * @Author: qq.yang
 * @Date: 2024/2/26 14:33
 */
internal class CustomTagHandler : TagHandler {

    companion object {
        const val HANDLE_TAG = "qqTag"
    }

    private var startIndex = 0
    private var stopIndex = 0
    private val attributes = HashMap<String, String>()

    override fun handleTag(
        opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader?
    ) {
        Logs.i("tagHandler -----> opening: $opening, tag: $tag, output: $output, xmlReader: ${xmlReader?.javaClass}")

        processAttributes(xmlReader)
        if (tag.equals(HANDLE_TAG, ignoreCase = true)) {
            if (opening) {
                startSpan(tag, output, xmlReader)
            } else {
                endSpan(tag, output, xmlReader)
                attributes.clear()
            }
        }
    }

    private fun startSpan(tag: String?, output: Editable, xmlReader: XMLReader?) {
        startIndex = output.length
    }

    private fun endSpan(tag: String?, output: Editable, xmlReader: XMLReader?) {
        stopIndex = output.length
        val style = attributes["style"]
        if (!style.isNullOrBlank()) {
            analysisStyle(startIndex, stopIndex, output, style)
        }
    }

    private fun processAttributes(xmlReader: XMLReader?) {
        xmlReader ?: return
        try {
            val elementField = xmlReader.javaClass.getDeclaredField("theNewElement")
            elementField.isAccessible = true
            val element = elementField[xmlReader]
            val attsField = element.javaClass.getDeclaredField("theAtts")
            attsField.isAccessible = true
            val atts = attsField[element]
            val dataField = atts.javaClass.getDeclaredField("data")
            dataField.isAccessible = true
            val data = dataField[atts] as Array<String>
            val lengthField = atts.javaClass.getDeclaredField("length")
            lengthField.isAccessible = true
            val len = lengthField[atts] as Int
            for (i in 0 until len) attributes[data[i * 5 + 1]] = data[i * 5 + 4]
        } catch (e: Exception) {
            // ignore.
        }
    }

    /**
     * 解析style属性
     */
    private fun analysisStyle(startIndex: Int, stopIndex: Int, editable: Editable, style: String?) {
        Logs.i("tagHandler -----> analysisStyle -----> style:$style")
        style ?: return
        val attrArray = style.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
        val attrMap: MutableMap<String, String> = HashMap()
        for (attr in attrArray) {
            val keyValueArray = attr.split(":".toRegex()).dropLastWhile { it.isEmpty() }
                .toTypedArray()
            if (keyValueArray.size == 2) {
                // 记住要去除前后空格
                attrMap[keyValueArray[0].trim { it <= ' ' }] =
                    keyValueArray[1].trim { it <= ' ' }
            }
        }
        Logs.i("tagHandler -----> analysisStyle -----> attrMap:$attrMap")

        val fontSizeAttr = attrMap["font-size"]
        val fontSize = fontSizeAttr?.removeSuffix("px")?.toFloatOrNull()?.dp
        fontSize?.let {
            editable.setSpan(
                AbsoluteSizeSpan(fontSize.toInt()),
                startIndex,
                stopIndex,
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
            )
        }

        val colorAttr = attrMap["color"]
        val fontColor = colorAttr?.let { parseColor(it) }
        fontColor?.let {
            editable.setSpan(
                ForegroundColorSpan(fontColor),
                startIndex,
                stopIndex,
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
            )
        }
    }

    // rgb(64, 169, 255) -> Color
    private fun parseColor(rgbString: String): Int {
        Logs.i("tagHandler -----> parseColor -----> rgbString:$rgbString")

        // 移除字符串中的 "rgb(" 和 ")"
        val cleanedString = rgbString.replace("rgb(", "").replace(")", "")

        // 拆分字符串以获取红、绿、蓝分量值
        val components = cleanedString.split(", ").map { it.toInt() }
        val (red, green, blue) = components

        // 构建颜色值并返回
        return Color.rgb(red, green, blue)
    }
}
相关推荐
skiy10 分钟前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
小小小点43 分钟前
Android四大常用布局详解与实战
android
MinQ2 小时前
binder和socket区别及原理
android
Ehtan_Zheng2 小时前
Jetpack Compose 中绘制发光边框的多种方式
android
智塑未来2 小时前
像素蛋糕安卓版 AI 专业修图全场景输出高清成片
android·人工智能
陆业聪4 小时前
让 Android 里的 AI 真正「干活」:Function Calling 工程实现全解
android·ai·kotlin
毕设源码-赖学姐4 小时前
【开题答辩全过程】以 基于Android的服装搭配APP为例,包含答辩的问题和答案
android
qq_717410014 小时前
Add Baidu NLP for projects without GMS packages
android
AI-小柒5 小时前
DataEyes 聚合平台 + Claude Code Max 编程实战
android·开发语言·人工智能·windows·python·macos·adb
cyforkk6 小时前
Spring Boot 3 集成 Swagger 踩坑实录:解决 doc.html 404 与 Unauthorized 拦截
spring boot·后端·html