TableWebViewActivity - WebView 表格实现详解
📋 项目概述
TableWebViewActivity 是一个使用 WebView 加载 HTML 实现表格功能的 Android Activity。该实现通过 HTML/CSS/JavaScript 来渲染表格,相比传统的 RecyclerView 实现,具有更好的样式控制能力和更流畅的用户体验。支持大量数据(1000+条)的分批加载,避免性能卡顿。
✨ 功能特性
- HTML/CSS 表格渲染:使用 WebView 加载 HTML 实现表格,样式控制更灵活
- 圆角表格设计:整个表格容器具有 16px 圆角,视觉效果更美观
- 完整表格边框:表格外部有 2px 边框,内部每个单元格都有 1px 边框
- 白色背景:所有单元格和表头都使用白色背景
- 高性能分批加载:支持 1000+ 条数据,使用分批渲染机制避免卡顿
- 动态更新:使用 JavaScript 动态添加/删除表格行,避免页面重新加载导致的抖动
- 展开/收起功能:支持展开显示全部数据和收起只显示初始数据
- 流畅的用户体验:加载过程中显示状态提示,支持自动滚动
🏗️ 架构设计
项目结构
TableWebViewActivity.kt
├── 数据管理
│ ├── allPersons: MutableList<Person> # 所有人员数据
│ ├── currentDisplayedCount: Int # 当前显示的数据条数
│ └── isExpanded: Boolean # 是否已展开
├── UI 组件
│ └── webView: WebView # WebView 组件
├── 核心方法
│ ├── generateData() # 生成测试数据
│ ├── setupWebView() # 配置 WebView
│ ├── loadInitialData() # 加载初始数据
│ ├── generateHtml() # 生成 HTML 内容
│ ├── expandListInBatches() # 分批展开数据
│ └── collapseList() # 收起数据
└── JavaScript 接口
└── WebAppInterface # Android-JavaScript 桥接
数据流程
用户点击按钮
↓
JavaScript 调用 Android 接口
↓
Android 处理逻辑(展开/收起)
↓
分批加载数据
↓
JavaScript 动态更新 DOM
↓
表格平滑更新(无抖动)
📖 代码详解
1. 类定义和属性
class TableWebViewActivity : AppCompatActivity() {
private lateinit var webView: WebView
private val allPersons = mutableListOf<Person>()
private var isExpanded = false
private var currentDisplayedCount = 0
private val handler = Handler(Looper.getMainLooper())
companion object {
private const val INITIAL_ITEM_COUNT = 10 // 初始显示的数据条数
private const val TOTAL_ITEM_COUNT = 1000 // 总数据量
private const val BATCH_SIZE = 100 // 每批渲染的数据量
private const val BATCH_DELAY_MS = 16L // 每批之间的延迟(约一帧时间)
}
}
属性说明:
webView: WebView 组件,用于显示 HTML 内容allPersons: 存储所有人员数据的列表isExpanded: 标记当前是否处于展开状态currentDisplayedCount: 记录当前已显示的数据条数handler: 主线程 Handler,用于异步更新 UI
常量说明:
INITIAL_ITEM_COUNT: 初始显示 10 条数据TOTAL_ITEM_COUNT: 总共生成 1000 条测试数据BATCH_SIZE: 每批加载 100 条数据BATCH_DELAY_MS: 每批之间延迟 16ms(约一帧时间,60fps)
2. 初始化方法
onCreate()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_table_webview)
initViews()
generateData()
setupWebView()
loadInitialData()
}
执行流程:
- 设置布局文件
- 初始化视图组件
- 生成测试数据
- 配置 WebView
- 加载初始数据
initViews()
private fun initViews() {
webView = findViewById(R.id.webView)
}
简单的视图初始化,获取 WebView 引用。
generateData()
private fun generateData() {
// 生成1000条测试数据
val names = listOf("张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十")
val genders = listOf("男", "女")
val hobbies = listOf("阅读", "运动", "音乐", "旅游", "摄影", "编程", "绘画", "烹饪")
for (i in 1..TOTAL_ITEM_COUNT) {
allPersons.add(
Person(
id = i,
name = "${names[i % names.size]}${i}",
age = 20 + (i % 50),
gender = genders[i % genders.size],
hobby = hobbies[i % hobbies.size]
)
)
}
}
功能:生成 1000 条测试数据
数据特点:
- 名字:从预定义列表中循环选择,并添加序号
- 年龄:20-69 岁之间循环
- 性别:男/女循环
- 爱好:从预定义列表中循环选择
3. WebView 配置
setupWebView()
private fun setupWebView() {
webView.settings.apply {
javaScriptEnabled = true // 启用 JavaScript
domStorageEnabled = true // 启用 DOM 存储
loadWithOverviewMode = true // 以概览模式加载
useWideViewPort = true // 使用宽视口
}
// 添加 JavaScript 接口
webView.addJavascriptInterface(WebAppInterface(), "Android")
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
// 页面加载完成后,可以执行 JavaScript
}
}
}
配置说明:
javaScriptEnabled = true: 必须启用,否则无法执行 JavaScriptdomStorageEnabled = true: 启用本地存储,用于存储数据loadWithOverviewMode = true: 以概览模式加载,适合移动设备useWideViewPort = true: 使用宽视口,适配不同屏幕尺寸
JavaScript 接口:
- 通过
addJavascriptInterface()添加接口,允许 JavaScript 调用 Android 方法 - 接口名称为 "Android",在 JavaScript 中通过
Android.expandTable()调用
4. HTML 生成
generateHtml()
这是核心方法,生成完整的 HTML 页面:
private fun generateHtml(persons: List<Person>, isExpanded: Boolean): String {
val tableRows = StringBuilder()
// 表头
tableRows.append("""
<tr>
<th>名字</th>
<th>年龄</th>
<th>性别</th>
<th>爱好</th>
</tr>
""".trimIndent())
// 数据行
persons.forEach { person ->
tableRows.append("""
<tr>
<td>${escapeHtml(person.name)}</td>
<td>${person.age}</td>
<td>${escapeHtml(person.gender)}</td>
<td>${escapeHtml(person.hobby)}</td>
</tr>
""".trimIndent())
}
val buttonText = if (isExpanded) "收起" else "展开更多"
return """...HTML 内容..."""
}
HTML 结构:
- 表头行:包含"名字"、"年龄"、"性别"、"爱好"四个列
- 数据行:循环添加每个 Person 对象的数据
- 按钮:根据展开状态显示不同的文本
CSS 样式特点:
.table-container: 白色背景,16px 圆角,2px 边框table: 100% 宽度,边框合并th, td: 12px 内边距,居中对齐,1px 边框,白色背景.expand-button: Material Design 风格的按钮
JavaScript 函数:
expandTable(): 处理按钮点击,调用 Android 接口addTableRows(rowsData): 动态添加表格行clearTableRows(): 清除所有数据行(保留表头)updateButton(text, enabled): 更新按钮文本和状态escapeHtml(text): HTML 转义函数
escapeHtml()
private fun escapeHtml(text: String): String {
return text
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
}
功能:转义 HTML 特殊字符,防止 XSS 攻击和显示错误
转义规则:
&→&<→<>→>"→"'→'
5. JavaScript 接口
WebAppInterface
inner class WebAppInterface {
@JavascriptInterface
fun expandTable() {
handler.post {
if (isExpanded) {
collapseList()
} else {
expandList()
}
}
}
}
功能:作为 Android 和 JavaScript 之间的桥接
关键点:
- 使用
@JavascriptInterface注解标记可被 JavaScript 调用的方法 - 使用
handler.post确保在主线程执行 UI 操作 - 根据
isExpanded状态决定展开或收起
6. 数据加载逻辑
loadInitialData()
private fun loadInitialData() {
currentDisplayedCount = if (isExpanded) {
allPersons.size
} else {
INITIAL_ITEM_COUNT
}
val dataPersons = allPersons.take(currentDisplayedCount)
val html = generateHtml(dataPersons, isExpanded)
webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null)
}
功能:加载初始数据到 WebView
逻辑:
- 根据
isExpanded状态决定显示的数据量 - 从
allPersons中取前 N 条数据 - 生成 HTML 内容
- 加载到 WebView
expandList()
private fun expandList() {
// 使用分批异步渲染,避免一次性更新导致卡顿
expandListInBatches()
}
简单的入口方法,调用分批展开逻辑。
expandListInBatches()
这是核心的分批加载方法:
private fun expandListInBatches() {
val remainingPersons = allPersons.subList(currentDisplayedCount, allPersons.size)
if (remainingPersons.isEmpty()) {
// 所有数据已加载完成
isExpanded = true
currentDisplayedCount = allPersons.size
updateButton("收起", true)
return
}
// 计算本次要添加的数据
val batch = remainingPersons.take(BATCH_SIZE)
currentDisplayedCount += batch.size
// 使用 JavaScript 动态添加行,避免重新加载整个页面
val rowsJson = batch.joinToString(",", "[", "]") { person ->
"""{"name":"${escapeHtmlForJs(person.name)}","age":${person.age},"gender":"${escapeHtmlForJs(person.gender)}","hobby":"${escapeHtmlForJs(person.hobby)}"}"""
}
val buttonText = if (currentDisplayedCount >= allPersons.size) "收起" else "正在加载..."
// 在主线程更新UI,但分批进行
handler.post {
// 使用 JavaScript 添加行,而不是重新加载整个页面
webView.evaluateJavascript("addTableRows($rowsJson);", null)
updateButton(buttonText, currentDisplayedCount >= allPersons.size)
// 当这一批数据渲染完成后,继续下一批
if (currentDisplayedCount < allPersons.size) {
// 延迟一帧时间,让UI有时间渲染
handler.postDelayed({
expandListInBatches()
}, BATCH_DELAY_MS)
} else {
// 所有数据加载完成
isExpanded = true
}
}
}
执行流程:
- 获取剩余未显示的数据
- 如果已全部加载,更新状态并返回
- 取一批数据(BATCH_SIZE 条)
- 将数据转换为 JSON 格式
- 使用
evaluateJavascript()调用 JavaScript 函数添加行 - 更新按钮状态
- 如果还有数据,延迟后继续下一批
关键优化:
- 动态更新:使用 JavaScript 动态添加行,而不是重新加载整个页面
- 分批加载:每批 100 条,避免一次性加载导致卡顿
- 延迟渲染:每批之间延迟 16ms,让 UI 有时间渲染
updateButton()
private fun updateButton(text: String, enabled: Boolean) {
val js = "updateButton('$text', $enabled);"
webView.evaluateJavascript(js, null)
}
功能:通过 JavaScript 更新按钮文本和启用状态
escapeHtmlForJs()
private fun escapeHtmlForJs(text: String): String {
return text
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
}
功能:转义 JavaScript 字符串中的特殊字符
转义规则:
\→\\"→\"\n→\\n\r→\\r\t→\\t
collapseList()
private fun collapseList() {
isExpanded = false
currentDisplayedCount = INITIAL_ITEM_COUNT
// 使用 JavaScript 清除多余的行,而不是重新加载整个页面
handler.post {
// 先清除所有数据行
webView.evaluateJavascript("clearTableRows();", null)
// 然后只添加初始的10条数据
val initialPersons = allPersons.take(INITIAL_ITEM_COUNT)
val rowsJson = initialPersons.joinToString(",", "[", "]") { person ->
"""{"name":"${escapeHtmlForJs(person.name)}","age":${person.age},"gender":"${escapeHtmlForJs(person.gender)}","hobby":"${escapeHtmlForJs(person.hobby)}"}"""
}
webView.evaluateJavascript("addTableRows($rowsJson);", null)
updateButton("展开更多", true)
// 滚动到顶部
handler.postDelayed({
webView.evaluateJavascript("window.scrollTo(0, 0);", null)
}, 100)
}
}
功能:收起表格,只显示初始的 10 条数据
执行流程:
- 更新状态标志
- 重置显示数量
- 使用 JavaScript 清除所有数据行
- 添加初始的 10 条数据
- 更新按钮状态
- 延迟后滚动到顶部
关键优化:
- 动态更新:使用 JavaScript 清除和添加行,避免页面重新加载
- 平滑过渡:操作在短时间内完成,用户体验流畅
🚀 使用说明
基本使用
-
启动 Activity:
val intent = Intent(this, TableWebViewActivity::class.java) startActivity(intent) -
初始状态:
- 表格显示前 10 条数据
- 底部显示"展开更多"按钮
-
展开操作:
- 点击"展开更多"按钮
- 按钮文本变为"正在加载...",按钮禁用
- 数据分批加载(每批 100 条,间隔 16ms)
- 加载完成后,按钮文本变为"收起",按钮启用
-
收起操作:
- 点击"收起"按钮
- 表格恢复到初始状态(只显示前 10 条)
- 自动滚动到顶部
自定义配置
修改初始显示数量
companion object {
private const val INITIAL_ITEM_COUNT = 10 // 修改为你想要的数量
}
修改总数据量
companion object {
private const val TOTAL_ITEM_COUNT = 1000 // 修改为你想要的数量
}
调整分批加载参数
companion object {
private const val BATCH_SIZE = 100 // 每批加载的数据量
private const val BATCH_DELAY_MS = 16L // 每批之间的延迟(毫秒)
}
参数调优建议:
BATCH_SIZE:根据设备性能调整- 低端设备:50-100
- 中端设备:100-200
- 高端设备:200-500
BATCH_DELAY_MS:通常设置为 16ms(60fps 的一帧时间)
自定义表格样式
修改 generateHtml() 方法中的 CSS:
.table-container {
background-color: #FFFFFF; // 修改背景色
border-radius: 16px; // 修改圆角大小
border: 2px solid #CCCCCC; // 修改边框样式
}
自定义数据源
修改 generateData() 方法,从网络或数据库加载:
private fun generateData() {
// 从网络加载
// viewModel.loadData { data ->
// allPersons.addAll(data)
// loadInitialData()
// }
// 或从数据库加载
// allPersons.addAll(database.getAllPersons())
}
⚡ 性能优化
1. 分批加载
原理:将大量数据分成小批次,每批之间延迟一帧时间
优势:
- 避免一次性加载导致 UI 线程阻塞
- 保持 60fps 的流畅帧率
- 用户可以随时看到加载进度
实现:
val batch = remainingPersons.take(BATCH_SIZE)
handler.postDelayed({
expandListInBatches()
}, BATCH_DELAY_MS)
2. JavaScript 动态更新
原理:使用 JavaScript DOM 操作,而不是重新加载整个 HTML 页面
优势:
- 避免页面重新加载导致的抖动
- 保持滚动位置
- 更快的更新速度
实现:
webView.evaluateJavascript("addTableRows($rowsJson);", null)
3. HTML 转义
原理:转义特殊字符,防止 XSS 攻击和显示错误
实现:
private fun escapeHtml(text: String): String {
return text
.replace("&", "&")
.replace("<", "<")
// ...
}
4. 主线程操作
原理:所有 UI 更新都在主线程执行
实现:
handler.post {
// UI 更新操作
}
🔧 技术细节
JavaScript 接口
添加接口
webView.addJavascriptInterface(WebAppInterface(), "Android")
注意事项:
- 接口名称 "Android" 在 JavaScript 中通过
Android.methodName()调用 - 方法必须使用
@JavascriptInterface注解 - 方法必须在主线程执行 UI 操作
调用 JavaScript
webView.evaluateJavascript("addTableRows($rowsJson);", null)
参数说明:
- 第一个参数:JavaScript 代码字符串
- 第二个参数:回调函数(可选)
HTML 生成
字符串模板
使用 Kotlin 的三引号字符串(triple-quoted string)生成 HTML:
return """
<!DOCTYPE html>
<html>
...
</html>
""".trimIndent()
优势:
- 保持代码格式
- 易于阅读和维护
- 支持多行字符串
字符串插值
在 HTML 中插入动态内容:
<td>${escapeHtml(person.name)}</td>
注意事项:
- 必须转义 HTML 特殊字符
- 使用
escapeHtml()函数
数据转换
JSON 格式
将 Person 对象转换为 JSON 格式:
val rowsJson = batch.joinToString(",", "[", "]") { person ->
"""{"name":"${escapeHtmlForJs(person.name)}","age":${person.age},...}"""
}
格式:
[
{"name":"张三1","age":20,"gender":"男","hobby":"阅读"},
{"name":"李四2","age":21,"gender":"女","hobby":"运动"},
...
]
📝 注意事项
1. 线程安全
- 所有 UI 操作必须在主线程执行
- 使用
Handler.post()确保线程安全
2. 内存管理
- 大量数据时注意内存使用
- 考虑使用分页加载或虚拟滚动
3. 网络安全
- 如果从网络加载 HTML,注意 XSS 攻击
- 始终转义用户输入
4. WebView 配置
- 必须启用 JavaScript
- 生产环境考虑禁用文件访问等安全设置
5. 性能考虑
- 避免一次性加载过多数据
- 使用分批加载机制
- 考虑使用虚拟滚动(Virtual Scrolling)
🐛 常见问题
Q: 为什么使用 WebView 而不是 RecyclerView?
A: WebView 的优势:
- 样式控制更灵活(CSS)
- 表格渲染更稳定
- 跨平台兼容性好
- 可以实现复杂的表格样式
Q: 如何解决页面抖动问题?
A: 使用 JavaScript 动态更新 DOM,而不是重新加载整个页面:
webView.evaluateJavascript("addTableRows($rowsJson);", null)
Q: 如何优化大量数据的加载?
A:
- 使用分批加载(BATCH_SIZE)
- 每批之间延迟(BATCH_DELAY_MS)
- 使用虚拟滚动(Virtual Scrolling)
Q: JavaScript 接口不工作怎么办?
A: 检查:
- 是否启用了 JavaScript:
javaScriptEnabled = true - 方法是否有
@JavascriptInterface注解 - 接口名称是否正确
- 是否在主线程调用
Q: 如何自定义表格样式?
A: 修改 generateHtml() 方法中的 CSS 部分:
.table-container {
/* 自定义样式 */
}
Q: 如何处理特殊字符?
A: 使用转义函数:
- HTML 转义:
escapeHtml() - JavaScript 转义:
escapeHtmlForJs()
📄 相关文件
TableWebViewActivity.kt- 主 Activity 文件activity_table_webview.xml- 布局文件Person.kt- 数据模型
🔗 相关文档
👨💻 全部代码实现
TableWebViewActivity 项目
```
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
class TableWebViewActivity : AppCompatActivity() {
private lateinit var webView: WebView
private val allPersons = mutableListOf<Person>()
private var isExpanded = false
private var currentDisplayedCount = 0
private val handler = Handler(Looper.getMainLooper())
companion object {
private const val INITIAL_ITEM_COUNT = 10
private const val TOTAL_ITEM_COUNT = 1000
private const val BATCH_SIZE = 100 // 每批渲染的数据量
private const val BATCH_DELAY_MS = 16L // 每批之间的延迟(约一帧时间)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_table_webview)
initViews()
generateData()
setupWebView()
loadInitialData()
}
private fun initViews() {
webView = findViewById(R.id.webView)
}
private fun generateData() {
// 生成1000条测试数据
val names = listOf("张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十")
val genders = listOf("男", "女")
val hobbies = listOf("阅读", "运动", "音乐", "旅游", "摄影", "编程", "绘画", "烹饪")
for (i in 1..TOTAL_ITEM_COUNT) {
allPersons.add(
Person(
id = i,
name = "${names[i % names.size]}${i}",
age = 20 + (i % 50),
gender = genders[i % genders.size],
hobby = hobbies[i % hobbies.size]
)
)
}
}
private fun setupWebView() {
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
useWideViewPort = true
}
// 添加 JavaScript 接口
webView.addJavascriptInterface(WebAppInterface(), "Android")
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
// 页面加载完成后,可以执行 JavaScript
}
}
}
private fun loadInitialData() {
currentDisplayedCount = if (isExpanded) {
allPersons.size
} else {
INITIAL_ITEM_COUNT
}
val dataPersons = allPersons.take(currentDisplayedCount)
val html = generateHtml(dataPersons, isExpanded)
webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null)
}
private fun generateHtml(persons: List<Person>, isExpanded: Boolean): String {
val tableRows = StringBuilder()
// 表头
tableRows.append("""
<tr>
<th>名字</th>
<th>年龄</th>
<th>性别</th>
<th>爱好</th>
</tr>
""".trimIndent())
// 数据行
persons.forEach { person ->
tableRows.append("""
<tr>
<td>${escapeHtml(person.name)}</td>
<td>${person.age}</td>
<td>${escapeHtml(person.gender)}</td>
<td>${escapeHtml(person.hobby)}</td>
</tr>
""".trimIndent())
}
val buttonText = if (isExpanded) "收起" else "展开更多"
return """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
padding: 16px;
background-color: #f5f5f5;
}
.table-container {
background-color: #FFFFFF;
border-radius: 16px;
overflow: hidden;
border: 2px solid #CCCCCC;
margin-bottom: 16px;
}
table {
width: 100%;
border-collapse: collapse;
background-color: #FFFFFF;
}
th, td {
padding: 12px;
text-align: center;
border: 1px solid #CCCCCC;
background-color: #FFFFFF;
}
th {
font-weight: bold;
}
.expand-button {
width: calc(100% - 32px);
padding: 16px;
margin: 16px;
background-color: #6200EE;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
}
.expand-button:disabled {
background-color: #CCCCCC;
cursor: not-allowed;
}
.expand-button:active {
background-color: #3700B3;
}
</style>
</head>
<body>
<div class="table-container">
<table id="dataTable">
$tableRows
</table>
</div>
<button class="expand-button" id="expandButton" onclick="expandTable()">
$buttonText
</button>
<script>
function expandTable() {
var button = document.getElementById('expandButton');
button.disabled = true;
button.textContent = '正在加载...';
Android.expandTable();
}
function addTableRows(rowsData) {
var table = document.getElementById('dataTable');
rowsData.forEach(function(row) {
var tr = document.createElement('tr');
tr.innerHTML = '<td>' + escapeHtml(row.name) + '</td>' +
'<td>' + row.age + '</td>' +
'<td>' + escapeHtml(row.gender) + '</td>' +
'<td>' + escapeHtml(row.hobby) + '</td>';
table.appendChild(tr);
});
}
function clearTableRows() {
var table = document.getElementById('dataTable');
// 保留表头,删除所有数据行
while (table.rows.length > 1) {
table.deleteRow(1);
}
}
function updateButton(text, enabled) {
var button = document.getElementById('expandButton');
button.textContent = text;
button.disabled = !enabled;
}
function escapeHtml(text) {
var map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
</script>
</body>
</html>
""".trimIndent()
}
private fun escapeHtml(text: String): String {
return text
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace(""", """)
.replace("'", "'")
}
inner class WebAppInterface {
@JavascriptInterface
fun expandTable() {
handler.post {
if (isExpanded) {
collapseList()
} else {
expandList()
}
}
}
}
private fun expandList() {
// 使用分批异步渲染,避免一次性更新导致卡顿
expandListInBatches()
}
private fun expandListInBatches() {
val remainingPersons = allPersons.subList(currentDisplayedCount, allPersons.size)
if (remainingPersons.isEmpty()) {
// 所有数据已加载完成
isExpanded = true
currentDisplayedCount = allPersons.size
updateButton("收起", true)
return
}
// 计算本次要添加的数据
val batch = remainingPersons.take(BATCH_SIZE)
currentDisplayedCount += batch.size
// 使用 JavaScript 动态添加行,避免重新加载整个页面
val rowsJson = batch.joinToString(",", "[", "]") { person ->
"""{"name":"${escapeHtmlForJs(person.name)}","age":${person.age},"gender":"${escapeHtmlForJs(person.gender)}","hobby":"${escapeHtmlForJs(person.hobby)}"}"""
}
val buttonText = if (currentDisplayedCount >= allPersons.size) "收起" else "正在加载..."
// 在主线程更新UI,但分批进行
handler.post {
// 使用 JavaScript 添加行,而不是重新加载整个页面
webView.evaluateJavascript("addTableRows($rowsJson);", null)
updateButton(buttonText, currentDisplayedCount >= allPersons.size)
// 当这一批数据渲染完成后,继续下一批
if (currentDisplayedCount < allPersons.size) {
// 延迟一帧时间,让UI有时间渲染
handler.postDelayed({
expandListInBatches()
}, BATCH_DELAY_MS)
} else {
// 所有数据加载完成
isExpanded = true
}
}
}
private fun updateButton(text: String, enabled: Boolean) {
val js = "updateButton('$text', $enabled);"
webView.evaluateJavascript(js, null)
}
private fun escapeHtmlForJs(text: String): String {
return text
.replace("\", "\\")
.replace(""", "\"")
.replace("\n", "\n")
.replace("\r", "\r")
.replace("\t", "\t")
}
private fun collapseList() {
isExpanded = false
currentDisplayedCount = INITIAL_ITEM_COUNT
// 使用 JavaScript 清除多余的行,而不是重新加载整个页面
handler.post {
// 先清除所有数据行
webView.evaluateJavascript("clearTableRows();", null)
// 然后只添加初始的10条数据
val initialPersons = allPersons.take(INITIAL_ITEM_COUNT)
val rowsJson = initialPersons.joinToString(",", "[", "]") { person ->
"""{"name":"${escapeHtmlForJs(person.name)}","age":${person.age},"gender":"${escapeHtmlForJs(person.gender)}","hobby":"${escapeHtmlForJs(person.hobby)}"}"""
}
webView.evaluateJavascript("addTableRows($rowsJson);", null)
updateButton("展开更多", true)
// 滚动到顶部
handler.postDelayed({
webView.evaluateJavascript("window.scrollTo(0, 0);", null)
}, 100)
}
}
}
---
**最后更新**:2025年12月27日