效果如下:
代码实现:
// 列的默认宽度(dp)
private val defaultColumnWidthDp = 40
private lateinit var excelTable: ExcelTable
/**
* 初始化表格
*/
private fun initTableView() {
// 初始化Excel表格
excelTable = ExcelTable()
// 初始化按钮事件
setupButtons()
// 添加初始行列
initTable()
// 更新表格显示
updateTableDisplay()
}
// 初始化表格为3行4列,第一列为标题列
private fun initTable() {
// 添加3列,都是文本类型
excelTable.addColumn(ColumnType.TEXT, CellData.TextData("工作地点"))
excelTable.addColumn(ColumnType.TEXT, CellData.TextData("工作内容"))
excelTable.addColumn(ColumnType.IMAGE, CellData.TextData("工作图片"))
// 添加4行数据(包括标题行)
// 已经添加了第一行作为标题行,再添加3行数据行
// 已添加1行(标题行),再添加3行数据行,总共4行
repeat(3) { rowIndex ->
val row = excelTable.addRow()
// 为数据行设置默认值
for (col in 0 until 3) {
val data = when (excelTable.getColumnType(col)) {
ColumnType.TEXT -> CellData.TextData("")
ColumnType.IMAGE -> CellData.ImageData(null)
null -> null
}
data?.let { excelTable.setCellData(row, col, it) }
}
}
}
// 设置按钮点击事件
private fun setupButtons() {
mBinding.btnAddRow.singleClick {
excelTable.addRow()
updateTableDisplay()
}
mBinding.btnAddImageColumn.singleClick {
if (excelTable.getColumnCount() >= 3) {
toast("目前最多支持3列!")
return@singleClick
}
PuzzleAddColumPopup.newInstance { content, type ->
excelTable.addColumn(type, CellData.TextData(content))
updateTableDisplay()
}.show(supportFragmentManager, "PuzzleAddColumPopup")
}
}
//表格图片数
private var tableImg = 0
// 更新表格显示
private fun updateTableDisplay() {
mBinding.tableContainer.removeAllViews()
tableImg = 0
val rowCount = excelTable.getRowCount()
val colCount = excelTable.getColumnCount()
if (colCount == 0 || rowCount == 0) return
// 默认行高(转换为像素)
val defaultRowHeightPx = excelTable.defaultRowHeight.dp
// 计算列宽度
val columnWidthPx = if (colCount <= 3) {
// 小于等于3列时,使用权重均分宽度
null // 用null表示使用权重模式
} else {
// 大于3列时,使用固定宽度
defaultColumnWidthDp.dp
}
for (row in 0 until rowCount) {
val rowLayout = LinearLayout(this)
rowLayout.orientation = LinearLayout.HORIZONTAL
rowLayout.layoutParams = LinearLayout.LayoutParams(
DensityUtil.getPhoneWidth(mContext) - 60.dp,
LinearLayout.LayoutParams.WRAP_CONTENT
)
rowLayout.minimumHeight = defaultRowHeightPx
// 设置行背景色(第一行为标题行,使用特殊颜色)
if (row == 0) {
rowLayout.setBackgroundColor(Color.parseColor("#EAEAEA"))
}
for (col in 0 until colCount) {
val cellView = LayoutInflater.from(this)
.inflate(R.layout.layout_text_img_cell, rowLayout, false)
val textView = cellView.findViewById<TextView>(R.id.tvCellText)
val imageView = cellView.findViewById<ImageView>(R.id.ivCellImage)
// 设置单元格宽度
val cellLayoutParams = if (columnWidthPx == null) {
// 使用权重模式(小于等于3列)
LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.MATCH_PARENT,
1f // 等权重分配
)
} else {
// 使用固定宽度(大于3列)
LinearLayout.LayoutParams(
columnWidthPx,
LinearLayout.LayoutParams.MATCH_PARENT
)
}
cellView.layoutParams = cellLayoutParams
// 根据列类型显示不同内容
val columnType = excelTable.getColumnType(col)
val cellData = excelTable.getCellData(row, col)
when (columnType) {
ColumnType.TEXT -> {
textView.visibility = View.VISIBLE
imageView.visibility = View.GONE
textView.typeface = if (row == 0) {
// 标题行文本加粗
Typeface.DEFAULT_BOLD
} else {
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, // 宽度 wrap_content
LinearLayout.LayoutParams.MATCH_PARENT // 高度 wrap_content
)
params.gravity = Gravity.START
params.setMargins(4.dp)
textView.layoutParams = params
Typeface.DEFAULT
}
textView.text = (cellData as? CellData.TextData)?.value ?: ""
}
ColumnType.IMAGE -> {
// 图片列的标题行仍显示文本
if (row == 0) {
textView.visibility = View.VISIBLE
imageView.visibility = View.GONE
textView.typeface = Typeface.DEFAULT_BOLD
textView.text = (cellData as? CellData.TextData)?.value ?: "图片列"
} else {
textView.visibility = View.GONE
imageView.visibility = View.VISIBLE
// 创建LinearLayout.LayoutParams
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, // 宽度 wrap_content
LinearLayout.LayoutParams.MATCH_PARENT // 高度 wrap_content
)
params.gravity = Gravity.CENTER_VERTICAL
params.setMargins(4.dp)
imageView.adjustViewBounds = true
imageView.layoutParams = params
tableImg++
val imageResId = (cellData as? CellData.ImageData)?.imageResId
if (imageResId != null) {
Glide.with(mContext)
.load(imageResId)
.dontTransform()
.into(imageView)
}
imageView.requestLayout()
}
}
null -> {}
}
// 设置单元格点击事件
cellView.setOnClickListener {
showEditDialog(row, col)
}
rowLayout.addView(cellView)
}
mBinding.tableContainer.addView(rowLayout)
}
}
/**
* 加载指定行所有数据
*/
private fun loadRowDataForEditing(colum: Int) {
val tableStr = JSON.toJSONString(excelTable, SerializerFeature.WriteClassName)
PuzzleTableTitleDialog.newInstance(tableStr, colum) { ta ->
if (ta != null) {
excelTable = ta
}
updateTableDisplay()
}.show(supportFragmentManager, "PuzzleTableTitleDialog")
}
/**
* 编辑列表内容
*/
private fun editTable(row: Int, colum: Int) {
val tableStr = JSON.toJSONString(excelTable, SerializerFeature.WriteClassName)
PuzzleEditTableDialog.newInstance(tableStr, row, colum) {
if (it != null) {
excelTable = it
}
updateTableDisplay()
}.show(supportFragmentManager, "PuzzleEditTableDialog")
}
// 显示编辑对话框 row行 col 列
private fun showEditDialog(row: Int, col: Int) {
if (row == 0) {
loadRowDataForEditing(col)
} else {
editTable(row, col)
}
}
/*************添加/删除操作*****************/
//添加行
excelTable.addRow()
//添加列
excelTable.addColumn(type, CellData.TextData("标题"))
工具类 ColumnType.kt:
// 列类型枚举
enum class ColumnType {
TEXT, IMAGE
}
// 单元格数据密封类
@JSONType(seeAlso = [CellData.TextData::class, CellData.ImageData::class]) // 关键:指定子类
sealed class CellData {
@JSONType(typeName = "TextData")
data class TextData(val value: String?) : CellData()
@JSONType(typeName = "ImageData")
data class ImageData(val imageResId: Uri?) : CellData()
}
// Excel表格管理类
class ExcelTable {
// 存储表格数据
private val rows = mutableListOf<MutableList<CellData?>>()
// 存储列类型
private val columns = mutableListOf<ColumnType>()
// 默认行高(dp)
val defaultRowHeight = 40
// 公开getter,供FastJSON访问
// 为FastJSON添加的getter(返回不可变视图,但保留原始类型)
fun getRows(): List<List<CellData?>> = rows.toList() // 转换为List确保序列化
fun getColumns(): List<ColumnType> = columns.toList()
// 为反序列化添加的setter(必须与getter对应)
fun setRows(rows: List<List<CellData?>>) {
this.rows.clear()
this.rows.addAll(rows.map { it.toMutableList() }) // 转换为MutableList
}
fun setColumns(columns: List<ColumnType>) {
this.columns.clear()
this.columns.addAll(columns)
}
/**
* 修改列类型
*/
fun changeColumnsType(col1: Int, type: ColumnType, columnTitle: String?) {
// 验证列索引有效性
if (col1 < 0 || col1 >= columns.size) {
return
}
columns[col1] = type
rows[0][col1] = CellData.TextData(columnTitle)
// 交换每一行中对应列的单元格数据
rows.forEachIndexed { index, _ ->
if (index > 0) {
rows[index][col1] = CellData.TextData("")
}
}
}
// 交换两列数据(列索引从0开始)
fun swapColumns(col1: Int, col2: Int): Boolean {
// 验证列索引有效性
if (col1 < 0 || col2 < 0 || col1 >= columns.size || col2 >= columns.size || col1 == col2) {
return false
}
// 交换列类型
val tempType = columns[col1]
columns[col1] = columns[col2]
columns[col2] = tempType
// 交换每一行中对应列的单元格数据
rows.forEach { rowData ->
val tempData = rowData[col1]
rowData[col1] = rowData[col2]
rowData[col2] = tempData
}
return true
}
// 添加列并指定类型和标题
fun addColumn(type: ColumnType, headerData: CellData.TextData) {
columns.add(type)
// 为每一行添加对应单元格
if (rows.isEmpty()) {
// 如果还没有行,添加一行作为标题行
val newRow = mutableListOf<CellData?>()
newRow.add(headerData)
rows.add(newRow)
} else {
// 为现有行添加单元格
for (i in rows.indices) {
if (i == 0) {
// 标题行添加标题数据
rows[i].add(headerData)
} else {
// 数据行添加默认值
val defaultData = when (type) {
ColumnType.TEXT -> CellData.TextData("")
ColumnType.IMAGE -> CellData.ImageData(null)
}
rows[i].add(defaultData)
}
}
}
}
// 添加新行并返回行索引
fun addRow(): Int {
val newRow = mutableListOf<CellData?>()
// 为新行的每个列添加默认数据
columns.forEachIndexed { colIndex, type ->
val defaultData = when (type) {
ColumnType.TEXT -> CellData.TextData("")
ColumnType.IMAGE -> CellData.ImageData(null)
}
newRow.add(defaultData)
}
rows.add(newRow)
return rows.size - 1
}
// 删除最后一行
fun removeRow(index: Int) {
if (rows.size > 1) { // 保留至少标题行
rows.removeAt(index)
}
}
// 删除最后一列
fun removeColumn(index: Int) {
if (columns.isNotEmpty()) {
columns.removeAt(index)
// 移除每行的最后一个单元格
rows.forEach { it.removeAt(index) }
}
}
// 设置单元格数据
fun setCellData(row: Int, column: Int, data: CellData) {
if (row in rows.indices && column in columns.indices) {
// 验证数据类型是否与列类型匹配(标题行除外)
if (row != 0) {
val columnType = columns[column]
if ((columnType == ColumnType.TEXT && data !is CellData.TextData) ||
(columnType == ColumnType.IMAGE && data !is CellData.ImageData)
) {
throw IllegalArgumentException("数据类型与列类型不匹配")
}
}
rows[row][column] = data
}
}
// 获取单元格数据
fun getCellData(row: Int, column: Int): CellData? {
return if (row in rows.indices && column in columns.indices) {
rows[row][column]
} else null
}
// 获取列类型
fun getColumnType(column: Int): ColumnType? {
return if (column in columns.indices) columns[column] else null
}
// 获取行数
fun getRowCount() = rows.size
// 获取列数
fun getColumnCount() = columns.size
}
布局:
<ScrollView
android:id="@+id/sclTable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/rl_top">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<HorizontalScrollView
android:id="@+id/tabLayoutScroller"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/btnAddImageColumn">
<LinearLayout
android:id="@+id/tableContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</HorizontalScrollView>
<TextView
android:id="@+id/btnAddImageColumn"
android:layout_width="28dp"
android:layout_height="match_parent"
android:layout_alignBottom="@+id/tabLayoutScroller"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:background="@drawable/bg_008bff_stroke_1"
android:gravity="center"
android:text="添\n加\n一\n列"
android:textColor="@color/font_008bff"
android:textSize="14sp" />
<!-- 右侧添加列View:与表格高度相同 -->
<TextView
android:id="@+id/btnAddRow"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_below="@+id/tabLayoutScroller"
android:layout_alignEnd="@+id/tabLayoutScroller"
android:layout_alignParentStart="true"
android:background="@drawable/bg_008bff_stroke_1"
android:gravity="center"
android:text="添加一行"
android:textColor="@color/font_008bff"
android:textSize="14sp" />
</RelativeLayout>
</ScrollView>