Jetpack Compose-使用WebView加载展示Tex富文本

前言

Tex公式及富文本常见就是使用Webview加载。

添加依赖

这个主要用于使用模板生成HTML

1
2
3
dependencies {
implementation 'com.x5dev:chunk-templates:3.6.2'
}

添加模板

JS

assets/js 中添加相关JS

下载地址

https://github.com/KaTeX/KaTeX/releases

模板

模板文件要放在src/main/assets/themes文件夹下

注意文件必须以.chtml作为后缀

assets/themes 中添加 ztex.chtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Katex View</title>
<link rel="stylesheet" type="text/css" href="file:///android_asset/katex/katex.min.css">
<style type="text/css">
body {
margin : 0;
padding : 0;
font-size: {$fontSize}px !important;
color: {$textColor} !important;
-webkit-touch-callout: none!important;
-webkit-user-select: none!important;
user-select: none!important;
}
</style>
</head>
<body>
{$content}
<script type="text/javascript" src="file:///android_asset/katex/katex.min.js"></script>
<script type="text/javascript" src="file:///android_asset/katex/contrib/auto-render.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.body, {
delimiters : [
{left: '$$', right: '$$', display: true},
{left: '\\[', right: '\\]', display: true},
{left: '$', right: '$', display: false},
{left: '\\(', right: '\\)', display: false}
]
});
});
</script>
</body>
</html>

自定义组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.webkit.WebSettings
import android.webkit.WebView
import com.x5.template.Chunk
import com.x5.template.Theme
import com.x5.template.providers.AndroidTemplates

@SuppressLint("SetJavaScriptEnabled")
public class ZTexView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) :
WebView(context, attrs, defStyleAttr) {
val TAG = "ZTexView"
private val TAG_CONTENT: String = "content"
private val TAG_TEXT_COLOR: String = "textColor"
private val TAG_FONT_SIZE: String = "fontSize"
private val TAG_TP_NAME: String = "ztex"
private var mText: String = ""
private var mTextColor: Int = Color.BLACK
private var mFontSize: Int = 16

init {
settings.cacheMode = WebSettings.LOAD_NO_CACHE
settings.displayZoomControls = false
settings.builtInZoomControls = false
settings.setSupportZoom(false)
settings.useWideViewPort = true // 启用宽视口支持
settings.loadWithOverviewMode = true // 配合使用,让网页自适应屏幕

settings.javaScriptEnabled = true // 如果图片加载依赖JS
settings.allowFileAccess = true
settings.domStorageEnabled = true
settings.loadsImagesAutomatically = true // 确保自动加载图片
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW // 如果有混合内容

setBackgroundColor(Color.TRANSPARENT)

val mTypeArray =
context.obtainStyledAttributes(attrs, R.styleable.ZTexView, 0, 0)

try {
setTextColor(
mTypeArray.getColor(
R.styleable.ZTexView_textColor,
Color.BLACK
)
)
mFontSize = mTypeArray.getInt(R.styleable.ZTexView_fontSize, 16)
val text: String? = mTypeArray.getString(R.styleable.ZTexView_text)
text?.also {
setText(it)
}
} finally {
mTypeArray.recycle()
}
}

fun setText(text: String) {
mText = text
loadHTML()
}

fun setTextColor(color: Int) {
this.mTextColor = color
loadHTML()
}

fun setFontSize(size: Int) {
this.mFontSize = size
loadHTML()
}

fun getText(): String {
return mText
}

private fun getChunk(): Chunk {
return Theme(AndroidTemplates(context)).makeChunk(TAG_TP_NAME)
}

private fun getHexColor(intColor: Int): String {
return String.format("#%06X", 0xFFFFFF and intColor)
}

/**
* 加载页面
*/
private fun loadHTML() {
val mChunk: Chunk = getChunk()
mChunk.set(TAG_TEXT_COLOR, getHexColor(mTextColor))
mChunk.set(TAG_FONT_SIZE, mFontSize)
mChunk.set(TAG_CONTENT, mText)
this.loadDataWithBaseURL(null, mChunk.toString(), "text/html", "utf-8", "about:blank")
}
}

注意

下面的配置确保图片的正常加载

1
2
3
4
5
settings.javaScriptEnabled = true // 如果图片加载依赖JS
settings.allowFileAccess = true
settings.domStorageEnabled = true
settings.loadsImagesAutomatically = true // 确保自动加载图片
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW // 如果有混合内容

Compose封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import android.content.Context
import android.webkit.WebViewClient
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import cn.psvmc.ztexview.ZTexView

fun getTexView(context: Context): ZTexView {
val texView = ZTexView(context).apply {
setFontSize(16)
webViewClient = WebViewClient()
}
texView.layoutParams = android.view.ViewGroup.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
)
return texView
}

@Composable
fun ZTexViewComp(
htmlContent: String,
modifier: Modifier = Modifier
) {
var texView: ZTexView? = null
AndroidView(
modifier = modifier,
factory = { context ->
texView = getTexView(context)
texView
},
update = { texView ->
texView.setText(htmlContent)
}
)

DisposableEffect(Unit) {
// 组件销毁时释放WebView资源
onDispose {
texView?.stopLoading()
// 清除客户端引用
texView?.webViewClient = WebViewClient()
texView?.destroy()
}
}
}

注意

销毁时释放Webview资源,避免内存泄漏导致的加载异常。同时也能让网页中播放的音视频停止。

使用

1
ZTexViewComp(ques.answer, modifier = Modifier.weight(1f))