android 实现表格效果

发布于:2025-08-19 ⋅ 阅读:(15) ⋅ 点赞:(0)

效果如下:

代码实现:

  // 列的默认宽度(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>


网站公告

今日签到

点亮在社区的每一天
去签到