我有一个来自 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>"
我将非常感谢任何帮助!
您可以通过提供
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()
}