Android Compose:如何显示文本中带有图像标签的 HTML 文本?

问题描述 投票:0回答:1

我有一个来自 API 的字符串,其中包含 HTML 格式的文本,我需要使用 Compose 将其显示给用户。对于带有格式的简单文本,使用此功能非常简单:

@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 字符串还包含一个

img
标签,其中包含指向我需要加载并显示在文本中间的图像的链接。检索链接很简单,我假设我可以使用 Coil 或 Glide 来加载图像本身,但是,我不知道如何正确显示它,特别是因为它还有
width
height
的附加参数。 HTML 文本示例:

<p>\nThis is a paragraph with image (external src)\n
<img src=\"https://www.example.com/_next/image?url=some_image.png" width=\"380\" height=\"200\">
\n</p>\n
<p style=\"color: black; text-align: justify;\">Some more text as an example</p>"

我将非常感谢任何帮助!

android html kotlin android-jetpack-compose
1个回答
0
投票

您可以通过提供

Html.ImageGetter
HtmlCompat.fromHtml
来实现。

这是我们制作的一个例子。

首先,制作自定义图像 Getter。我在这个例子中使用线圈。

class CoilImageGetter(
    private val textView: TextView,
    private val imageLoader: ImageLoader = Coil.imageLoader(textView.context),
    private val sourceModifier: ((source: String) -> String)? = null,
    private val fixedImageWidth: Int,
) : Html.ImageGetter {

    override fun getDrawable(source: String): Drawable {
        val finalSource = sourceModifier?.invoke(source) ?: source
        val placeholder = ColorDrawable(Color.LTGRAY).toBitmap(100, 100)
        val drawablePlaceholder = DrawablePlaceHolder(textView.resources, placeholder)
        imageLoader.enqueue(
            ImageRequest.Builder(textView.context).data(finalSource).apply {
                target { drawable ->
                    drawablePlaceholder.updateDrawable(fixedImageWidth, drawable)
                    // invalidating the drawable doesn't seem to be enough...
                    textView.text = textView.text
                }
                scale(Scale.FIT)
            }.build()
        )
        // Since this loads async, we return a "blank" drawable, which we update later
        return drawablePlaceholder
    }

    private class DrawablePlaceHolder(
        resource: Resources,
        bitmap: Bitmap,
    ) : BitmapDrawable(resource, bitmap) {

        private var drawable: Drawable? = null

        override fun draw(canvas: Canvas) {
            drawable?.draw(canvas)
        }

        fun updateDrawable(width: Int, drawable: Drawable) {
            this.drawable = drawable
            val aspectRatio = drawable.intrinsicHeight.toFloat() / drawable.intrinsicWidth.toFloat()
            val height = (width.toFloat() * aspectRatio).toInt()
            drawable.setBounds(0, 0, width, height)
            setBounds(0, 0, width, height)
        }
    }
}

然后是 Html 可组合组件

@Composable
fun HtmlText(
    html: String,
    modifier: Modifier = Modifier,
    htmlFlags: Int = FROM_HTML_MODE_LEGACY,
    htmlTagHandler: TagHandler = ListTagHandler(),
    imageSourceModifier: ((source: String) -> String)? = null,
    @ColorRes color: Int,
    @StyleRes style: Int,
    typeface: Typeface? = null,
    onClicked: ((String) -> Unit)? = null
) {
    // This is used to determine the bounds of Drawable inside htmlText
    // which is handling by CoilImageGetter.
    var componentWidth by remember {
        mutableIntStateOf(0)
    }

    val text = remember(html) {
        if (htmlTagHandler is ListTagHandler) {
            ListTagHandler.changeListTag(html)
        } else {
            html
        }
    }

    AndroidView(
        factory = {
            AppCompatTextView(it)
        },
        modifier.onGloballyPositioned {
            componentWidth = it.size.width
        }.testTag("html-text"),
        update = { textView ->
            textView.text = text.parseAsHtml(
                htmlFlags,
                tagHandler = htmlTagHandler,
                imageGetter = CoilImageGetter(
                    textView,
                    sourceModifier = imageSourceModifier,
                    fixedImageWidth = componentWidth
                )
            )

            textView.gravity = Gravity.START
            textView.setTextColor(ContextCompat.getColor(textView.context, color))
            TextViewCompat.setTextAppearance(textView, style)
            textView.setLinkTextColor(
                ContextCompat.getColor(textView.context, SeragamR.color.color_action_default)
            )
            textView.typeface = typeface
            textView.handleUrlClicks(onClicked)
        },
    )
}

private fun AppCompatTextView.handleUrlClicks(onClicked: ((String) -> Unit)? = null) {
    linksClickable = true
    text = SpannableStringBuilder.valueOf(text).apply {
        getSpans(0, length, URLSpan::class.java).forEach {
            setSpan(
                object : ClickableSpan() {
                    override fun onClick(widget: View) {
                        onClicked?.invoke(it.url.trim())
                    }
                },
                getSpanStart(it),
                getSpanEnd(it),
                Spanned.SPAN_INCLUSIVE_EXCLUSIVE
            )
            removeSpan(it)
        }
    }
    movementMethod = LinkMovementMethod.getInstance()
}
© www.soinside.com 2019 - 2024. All rights reserved.