Android 轻松实现 增强版灵活的 滑动式表格视图

表格视图组件,支持:

  1. 无标题模式:只有数据行也可以正常滑动
  2. 两种滑动模式:固定第一列 或 全部滑动
  3. 全面的样式自定义能力
  4. 智能列宽计算

1. 无标题模式支持

  • 设置无标题 :调用 setHeaderData(null)setHeaderData(emptyList())
  • 自动调整
    • 隐藏标题行相关视图
    • 智能计算列宽时忽略标题行
    • 保持数据行正常显示和滑动

2. 两种滑动模式

  • 固定第一列模式

    scss 复制代码
    tableView.setScrollMode(FlexibleTableView.ScrollMode.FIXED_FIRST_COLUMN)
    • 第一列垂直固定
    • 标题行和内容区域可水平滚动
    • 适合需要固定标识列的场景
  • 全滑动模式

    scss 复制代码
    tableView.setScrollMode(FlexibleTableView.ScrollMode.FULL_SCROLL)
    • 整个表格可水平滚动
    • 标题行和内容区域同步滚动
    • 适合所有列同等重要的场景

3. 智能列宽计算

  • 等宽模式

    arduino 复制代码
    tableView.setEqualColumnWidth(true)
    • 所有列使用相同宽度
    • 宽度取所有列内容最大宽度
  • 自适应模式

    arduino 复制代码
    tableView.setEqualColumnWidth(false)
    • 每列根据内容计算宽度
    • 可设置最小宽度保证可读性
  • 最小宽度设置

    scss 复制代码
    // 设置第一列最小宽度
    tableView.setFirstColumnMinWidth(120) // 120dp
    
    // 设置其他列最小宽度
    tableView.setOtherColumnMinWidth(90) // 90dp

4. 全面的样式自定义

  • 标题行样式

    css 复制代码
    tableView.setHeaderTextColor(Color.WHITE)
    tableView.setHeaderBackgroundColor(Color.BLUE)
  • 第一列样式

    css 复制代码
    tableView.setFirstColumnTextColor(Color.DKGRAY)
    tableView.setFirstColumnBackgroundColor(Color.LTGRAY)
  • 内容区域样式

    css 复制代码
    tableView.setContentTextColor(Color.BLACK)
    tableView.setContentBackgroundColor(Color.WHITE)
  • 网格线样式

    css 复制代码
    tableView.setGridLineColor(Color.GRAY)

## 使用方法示例

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    private lateinit var tableView: FlexibleTableView
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        tableView = findViewById(R.id.tableView)
        
        // 1. 设置表格模式
        tableView.setScrollMode(FlexibleTableView.ScrollMode.FIXED_FIRST_COLUMN)
        
        // 2. 设置表格列宽配置
        tableView.setEqualColumnWidth(true) // 所有列等宽
        tableView.setFirstColumnMinWidth(120) // 第一列最小宽度120dp
        tableView.setOtherColumnMinWidth(90)  // 其他列最小宽度90dp
        
        // 3. 设置样式
        tableView.setHeaderTextColor(Color.WHITE)
        tableView.setHeaderBackgroundColor(Color.parseColor("#3F51B5"))
        tableView.setFirstColumnTextColor(Color.DKGRAY)
        tableView.setFirstColumnBackgroundColor(Color.parseColor("#E8EAF6"))
        tableView.setContentTextColor(Color.BLACK)
        tableView.setContentBackgroundColor(Color.parseColor("#F5F5F5"))
        tableView.setGridLineColor(Color.parseColor("#9E9E9E"))
        
        // 4. 场景1: 有标题行的情况
        setupWithHeaders()
        
        // 5. 场景2: 无标题行的情况
        setupWithoutHeaders()
        
        // 6. 添加切换按钮
        setupModeSwitchButton()
    }
    
    private fun setupWithHeaders() {
        // 有标题行的数据
        val headers = listOf("产品", "一月", "二月", "三月", "四月", "五月", "六月")
        tableView.setHeaderData(headers)
        
        val products = listOf(
            listOf("智能手机", "1250", "1380", "1520", "1670", "1820", "1980"),
            listOf("笔记本电脑", "780", "820", "890", "920", "950", "980"),
            listOf("平板电脑", "620", "680", "710", "750", "790", "820")
        )
        tableView.setRowData(products)
    }
    
    private fun setupWithoutHeaders() {
        // 无标题行的数据
        tableView.setHeaderData(null) // 不设置标题行
        
        val data = listOf(
            listOf("张三", "90", "85", "95", "88", "92"),
            listOf("李四", "88", "92", "90", "85", "90"),
            listOf("王五", "78", "80", "85", "90", "86"),
            listOf("赵六", "92", "90", "88", "92", "94"),
            listOf("钱七", "76", "85", "80", "78", "82")
        )
        tableView.setRowData(data)
    }
    
    private fun setupModeSwitchButton() {
        val switchButton: Button = findViewById(R.id.switchModeButton)
        switchButton.setOnClickListener {
            val newMode = if (tableView.getScrollMode() == FlexibleTableView.ScrollMode.FIXED_FIRST_COLUMN) {
                FlexibleTableView.ScrollMode.FULL_SCROLL
            } else {
                FlexibleTableView.ScrollMode.FIXED_FIRST_COLUMN
            }
            tableView.setScrollMode(newMode)
            
            switchButton.text = if (newMode == FlexibleTableView.ScrollMode.FIXED_FIRST_COLUMN) {
                "切换到全滑动模式"
            } else {
                "切换到固定第一列模式"
            }
        }
    }
}

完整实现代码

FlexibleTableView

scss 复制代码
class FlexibleTableView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

    enum class ScrollMode {
        FIXED_FIRST_COLUMN, // 固定第一列模式
        FULL_SCROLL        // 全部滑动模式
    }

    private val mContext: Context = context
    private lateinit var rvHeader: RecyclerView
    private lateinit var rvFirstColumn: RecyclerView
    private lateinit var rvItems: RecyclerView
    private lateinit var tvFirstHeader: TableCell
    private lateinit var headerAdapter: TableAdapter
    private lateinit var firstColumnAdapter: TableAdapter
    private lateinit var itemAdapter: TableAdapter
    private var headerList: List<String> = ArrayList()
    private val firstColumnList: MutableList<String> = ArrayList()
    private val itemList: MutableList<String> = ArrayList()
    
    // 样式配置
    @ColorInt private var headerTextColor = Color.WHITE
    @ColorInt private var headerBackgroundColor = Color.parseColor("#3F51B5")
    @ColorInt private var firstColumnTextColor = Color.DKGRAY
    @ColorInt private var firstColumnBackgroundColor = Color.parseColor("#E8EAF6")
    @ColorInt private var contentTextColor = Color.BLACK
    @ColorInt private var contentBackgroundColor = Color.parseColor("#F5F5F5")
    @ColorInt private var gridLineColor = Color.parseColor("#9E9E9E")
    
    // 宽度配置
    private var firstColumnMinWidth = dpToPx(120) // 第一列最小宽度
    private var otherColumnMinWidth = dpToPx(100) // 其他列最小宽度
    private var equalColumnWidth = true // 是否等宽显示
    
    // 滚动模式
    private var scrollMode = ScrollMode.FIXED_FIRST_COLUMN
    
    // 滚动位置缓存
    private var scrollX = 0
    private var scrollY = 0
    
    // 标题行可见性
    private var headerVisible = true

    init {
        initView()
    }

    private fun initView() {
        orientation = HORIZONTAL
        removeAllViews()
        
        when (scrollMode) {
            ScrollMode.FIXED_FIRST_COLUMN -> initFixedFirstColumnMode()
            ScrollMode.FULL_SCROLL -> initFullScrollMode()
        }
    }
    
    private fun initFixedFirstColumnMode() {
        // 固定第一列模式
        addView(createFixedColumnHeader())
        addView(createScrollableContentArea())
        
        setupAdapters()
        setupScrollSync()
    }
    
    private fun initFullScrollMode() {
        // 全滑动模式
        addView(createFullScrollContainer())
        
        setupAdapters()
        setupScrollSync()
    }
    
    private fun setupAdapters() {
        headerAdapter = TableAdapter(mContext)
        headerAdapter.isHeader(true)
        
        firstColumnAdapter = TableAdapter(mContext)
        itemAdapter = TableAdapter(mContext)
        
        if (::rvHeader.isInitialized) rvHeader.adapter = headerAdapter
        if (::rvFirstColumn.isInitialized) rvFirstColumn.adapter = firstColumnAdapter
        if (::rvItems.isInitialized) rvItems.adapter = itemAdapter
        
        if (::tvFirstHeader.isInitialized) {
            tvFirstHeader.setHeader(true)
        }
    }
    
    private fun setupScrollSync() {
        if (!::rvItems.isInitialized || !::rvFirstColumn.isInitialized) return
        
        rvFirstColumn.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                if (recyclerView.scrollState != RecyclerView.SCROLL_STATE_IDLE) {
                    rvItems.scrollBy(dx, dy)
                    scrollY += dy
                }
            }
        })

        rvItems.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                if (recyclerView.scrollState != RecyclerView.SCROLL_STATE_IDLE) {
                    rvFirstColumn.scrollBy(dx, dy)
                    scrollY += dy
                    scrollX += dx
                }
            }
        })
    }
    
    private fun createFixedColumnHeader(): LinearLayout {
        tvFirstHeader = TableCell(mContext, firstColumnMinWidth)
        tvFirstHeader.setGridLineColor(gridLineColor)
        tvFirstHeader.setTextColor(headerTextColor)
        tvFirstHeader.setHeaderBackgroundColor(headerBackgroundColor)
        tvFirstHeader.visibility = if (headerVisible) View.VISIBLE else View.GONE

        val lyHeader = LinearLayout(mContext)
        lyHeader.orientation = LinearLayout.VERTICAL
        lyHeader.layoutParams = LayoutParams(
            LayoutParams.WRAP_CONTENT, 
            LayoutParams.MATCH_PARENT
        )
        lyHeader.addView(tvFirstHeader)

        rvFirstColumn = RecyclerView(mContext)
        rvFirstColumn.layoutManager = LinearLayoutManager(mContext)
        lyHeader.addView(rvFirstColumn)

        return lyHeader
    }
    
    private fun createScrollableContentArea(): HorizontalScrollView {
        val layout = LinearLayout(mContext)
        layout.orientation = LinearLayout.VERTICAL
        layout.layoutParams = LayoutParams(
            LayoutParams.MATCH_PARENT, 
            LayoutParams.MATCH_PARENT
        )

        // 标题行容器
        rvHeader = RecyclerView(mContext)
        rvHeader.layoutParams = LayoutParams(
            LayoutParams.MATCH_PARENT, 
            LayoutParams.WRAP_CONTENT
        )
        rvHeader.visibility = if (headerVisible) View.VISIBLE else View.GONE
        
        val headerManager = LinearLayoutManager(mContext)
        headerManager.orientation = LinearLayoutManager.HORIZONTAL
        rvHeader.layoutManager = headerManager
        layout.addView(rvHeader)

        // 内容行容器
        rvItems = RecyclerView(mContext)
        rvItems.layoutParams = LayoutParams(
            LayoutParams.MATCH_PARENT, 
            LayoutParams.MATCH_PARENT
        )
        layout.addView(rvItems)

        val scrollView = HorizontalScrollView(mContext)
        scrollView.layoutParams = LayoutParams(
            LayoutParams.MATCH_PARENT, 
            LayoutParams.MATCH_PARENT
        )
        scrollView.addView(layout)
        scrollView.isFillViewport = true
        scrollView.overScrollMode = View.OVER_SCROLL_NEVER
        scrollView.isHorizontalScrollBarEnabled = false

        return scrollView
    }
    
    private fun createFullScrollContainer(): HorizontalScrollView {
        val container = LinearLayout(mContext)
        container.orientation = LinearLayout.VERTICAL
        container.layoutParams = LayoutParams(
            LayoutParams.MATCH_PARENT, 
            LayoutParams.MATCH_PARENT
        )
        
        // 标题行(可滑动)
        rvHeader = RecyclerView(mContext)
        rvHeader.layoutParams = LayoutParams(
            LayoutParams.MATCH_PARENT, 
            LayoutParams.WRAP_CONTENT
        )
        rvHeader.visibility = if (headerVisible) View.VISIBLE else View.GONE
        
        val headerManager = LinearLayoutManager(mContext)
        headerManager.orientation = LinearLayoutManager.HORIZONTAL
        rvHeader.layoutManager = headerManager
        container.addView(rvHeader)
        
        // 内容区域(包括第一列和其余列)
        val contentContainer = LinearLayout(mContext)
        contentContainer.orientation = LinearLayout.HORIZONTAL
        contentContainer.layoutParams = LayoutParams(
            LayoutParams.MATCH_PARENT, 
            LayoutParams.MATCH_PARENT
        )
        
        // 第一列(在完整滑动模式下也包含在可滚动区域)
        val firstColumnContainer = LinearLayout(mContext)
        firstColumnContainer.orientation = LinearLayout.VERTICAL
        firstColumnContainer.layoutParams = LayoutParams(
            LayoutParams.WRAP_CONTENT, 
            LayoutParams.MATCH_PARENT
        )
        
        tvFirstHeader = TableCell(mContext, firstColumnMinWidth)
        tvFirstHeader.setGridLineColor(gridLineColor)
        tvFirstHeader.setTextColor(headerTextColor)
        tvFirstHeader.setHeaderBackgroundColor(headerBackgroundColor)
        tvFirstHeader.visibility = if (headerVisible) View.VISIBLE else View.GONE
        firstColumnContainer.addView(tvFirstHeader)
        
        rvFirstColumn = RecyclerView(mContext)
        rvFirstColumn.layoutManager = LinearLayoutManager(mContext)
        firstColumnContainer.addView(rvFirstColumn)
        
        contentContainer.addView(firstColumnContainer)
        
        // 其余列
        rvItems = RecyclerView(mContext)
        rvItems.layoutParams = LayoutParams(
            LayoutParams.MATCH_PARENT, 
            LayoutParams.MATCH_PARENT
        )
        contentContainer.addView(rvItems)
        
        container.addView(contentContainer)
        
        val scrollView = HorizontalScrollView(mContext)
        scrollView.layoutParams = LayoutParams(
            LayoutParams.MATCH_PARENT, 
            LayoutParams.MATCH_PARENT
        )
        scrollView.addView(container)
        scrollView.isFillViewport = true
        scrollView.overScrollMode = View.OVER_SCROLL_NEVER
        scrollView.isHorizontalScrollBarEnabled = false
        scrollView.scrollTo(scrollX, 0)

        return scrollView
    }

    fun setHeaderData(headerData: List<String>?) {
        if (headerData == null || headerData.isEmpty()) {
            // 没有标题行
            headerVisible = false
            headerList = emptyList()
            
            if (::tvFirstHeader.isInitialized) {
                tvFirstHeader.visibility = View.GONE
            }
            if (::rvHeader.isInitialized) {
                rvHeader.visibility = View.GONE
            }
            
            headerAdapter.setItemList(emptyList())
            return
        }
        
        // 有标题行
        headerVisible = true
        headerList = ArrayList(headerData)
        val headers = ArrayList(headerData)
        
        if (::tvFirstHeader.isInitialized) {
            tvFirstHeader.text = headers[0]
            tvFirstHeader.visibility = View.VISIBLE
        }
        
        headers.removeAt(0)
        headerAdapter.setItemList(headers)
        
        if (::rvHeader.isInitialized) {
            rvHeader.visibility = View.VISIBLE
        }
        
        if (::rvItems.isInitialized) {
            rvItems.layoutManager = GridLayoutManager(mContext, headerList.size - 1)
        }
    }

    fun setRowData(rowDataList: List<List<String>>) {
        if (rowDataList.isEmpty()) {
            // 清空数据
            firstColumnList.clear()
            itemList.clear()
            firstColumnAdapter.setItemList(emptyList())
            itemAdapter.setItemList(emptyList())
            return
        }

        // 确定列数:取第一行数据除去第一列后的列数
        val columnCount = rowDataList[0].size - 1

        // 设置GridLayoutManager的列数
        if (::rvItems.isInitialized) {
            rvItems.layoutManager = GridLayoutManager(mContext, columnCount)
        }

        // 处理数据
        firstColumnList.clear()
        itemList.clear()
        addRowData(rowDataList)
    }

    fun addRowData(rowDataList: List<List<String>>) {
        val list = ArrayList(rowDataList)
        for (rowData in list) {
            val row = ArrayList(rowData)
            if (row.isNotEmpty()) {
                firstColumnList.add(row[0])
                row.removeAt(0)
                itemList.addAll(row)
            }
        }

        firstColumnAdapter.setItemList(firstColumnList)
        itemAdapter.setItemList(itemList)

        if (::rvFirstColumn.isInitialized && ::rvItems.isInitialized) {
            rvFirstColumn.scrollTo(0, scrollY)
            rvItems.scrollTo(scrollX, scrollY)
        }

        calculateColumnWidths()
    }
    
    private fun calculateColumnWidths() {
        // 计算第一列宽度
        val firstColData = firstColumnList.toMutableList()
        if (headerVisible) {
            firstColData.add(tvFirstHeader.text.toString())
        }
        val firstColWidth = calculateColumnWidth(firstColData, firstColumnMinWidth)
        
        // 计算其他列宽度
        val otherColWidth = if (equalColumnWidth) {
            val maxOtherWidth = if (headerVisible) {
                maxOf(
                    calculateColumnWidth(headerList, otherColumnMinWidth),
                    calculateColumnWidth(itemList, otherColumnMinWidth)
                )
            } else {
                calculateColumnWidth(itemList, otherColumnMinWidth)
            }
            maxOf(maxOtherWidth, otherColumnMinWidth)
        } else {
            if (headerVisible) {
                maxOf(
                    calculateColumnWidth(headerList, otherColumnMinWidth),
                    calculateColumnWidth(itemList, otherColumnMinWidth)
                )
            } else {
                calculateColumnWidth(itemList, otherColumnMinWidth)
            }
        }
        
        // 设置宽度
        if (::tvFirstHeader.isInitialized) {
            tvFirstHeader.width = firstColWidth
        }
        firstColumnAdapter.setItemWidth(firstColWidth)
        headerAdapter.setItemWidth(otherColWidth)
        itemAdapter.setItemWidth(otherColWidth)
    }
    
    private fun calculateColumnWidth(data: List<String>, minWidth: Int): Int {
        if (data.isEmpty()) return minWidth
        
        var maxWidth = minWidth
        val paint = Paint()
        paint.textSize = spToPx(14)
        
        for (text in data) {
            val textWidth = paint.measureText(text).toInt()
            val cellWidth = textWidth + dpToPx(20) // 加上内边距
            if (cellWidth > maxWidth) {
                maxWidth = cellWidth
            }
        }
        
        return maxWidth
    }

    fun getItemCount(): Int = firstColumnList.size * (if (headerVisible) headerList.size - 1 else 0)
    
    // 样式设置方法
    fun setHeaderTextColor(@ColorInt color: Int) {
        headerTextColor = color
        if (::tvFirstHeader.isInitialized) {
            tvFirstHeader.setTextColor(color)
        }
        headerAdapter.setTextColor(color)
    }
    
    fun setHeaderBackgroundColor(@ColorInt color: Int) {
        headerBackgroundColor = color
        if (::tvFirstHeader.isInitialized) {
            tvFirstHeader.setHeaderBackgroundColor(color)
        }
        headerAdapter.setBackgroundColor(color)
    }
    
    fun setFirstColumnTextColor(@ColorInt color: Int) {
        firstColumnTextColor = color
        firstColumnAdapter.setTextColor(color)
    }
    
    fun setFirstColumnBackgroundColor(@ColorInt color: Int) {
        firstColumnBackgroundColor = color
        firstColumnAdapter.setBackgroundColor(color)
    }
    
    fun setContentTextColor(@ColorInt color: Int) {
        contentTextColor = color
        itemAdapter.setTextColor(color)
    }
    
    fun setContentBackgroundColor(@ColorInt color: Int) {
        contentBackgroundColor = color
        itemAdapter.setBackgroundColor(color)
    }
    
    fun setGridLineColor(@ColorInt color: Int) {
        gridLineColor = color
        if (::tvFirstHeader.isInitialized) {
            tvFirstHeader.setGridLineColor(color)
        }
        headerAdapter.setGridLineColor(color)
        firstColumnAdapter.setGridLineColor(color)
        itemAdapter.setGridLineColor(color)
    }
    
    // 宽度设置方法
    fun setEqualColumnWidth(enabled: Boolean) {
        equalColumnWidth = enabled
        calculateColumnWidths()
    }
    
    fun setFirstColumnMinWidth(minWidthDp: Int) {
        firstColumnMinWidth = dpToPx(minWidthDp)
        calculateColumnWidths()
    }
    
    fun setOtherColumnMinWidth(minWidthDp: Int) {
        otherColumnMinWidth = dpToPx(minWidthDp)
        calculateColumnWidths()
    }
    
    // 滚动模式设置
    fun setScrollMode(mode: ScrollMode) {
        if (scrollMode != mode) {
            // 保存当前滚动位置
            scrollX = 0
            scrollY = 0
            
            scrollMode = mode
            initView()
            
            // 重新应用数据
            if (headerList.isNotEmpty()) {
                setHeaderData(headerList)
            }
            if (firstColumnList.isNotEmpty()) {
                setRowData(firstColumnList.map { listOf(it) })
            }
        }
    }
    
    fun getScrollMode(): ScrollMode = scrollMode
    
    // 单位转换工具
    private fun dpToPx(dp: Int): Int {
        return (dp * context.resources.displayMetrics.density).toInt()
    }
    
    private fun spToPx(sp: Int): Float {
        return sp * context.resources.displayMetrics.scaledDensity
    }
}

TableAdapter

kotlin 复制代码
@SuppressLint("NotifyDataSetChanged")
class TableAdapter(private val mContext: Context) : RecyclerView.Adapter<TableAdapter.MyViewHolder>() {

    private var mItemList: List<String> = ArrayList()
    private var itemWidth = 0
    private var isHeader = false
    private var textColor = Color.BLACK
    private var backgroundColor = Color.WHITE
    private var gridLineColor = Color.GRAY

    fun setItemWidth(width: Int) {
        itemWidth = width
        notifyDataSetChanged()
    }

    fun setTextColor(color: Int) {
        textColor = color
        notifyDataSetChanged()
    }
    
    fun setBackgroundColor(color: Int) {
        backgroundColor = color
        notifyDataSetChanged()
    }
    
    fun setGridLineColor(color: Int) {
        gridLineColor = color
        notifyDataSetChanged()
    }

    fun setItemList(itemList: List<String>) {
        mItemList = itemList
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val cell = TableCell(mContext, itemWidth)
        cell.setHeader(isHeader)
        cell.setGridLineColor(gridLineColor)
        return MyViewHolder(cell)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = mItemList[position]
        val tv = holder.itemView as TableCell
        tv.text = item
        tv.setTextColor(textColor)
        tv.setCellBackgroundColor(backgroundColor)
        
        // 如果是表头行,应用特殊样式
        if (isHeader) {
            tv.setTextColor(textColor)
            tv.setHeaderBackgroundColor(backgroundColor)
        }
    }

    override fun getItemCount(): Int = mItemList.size

    fun isHeader(isHeader: Boolean) {
        this.isHeader = isHeader
    }

    inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}

TableCell

kotlin 复制代码
class TableCell @JvmOverloads constructor(
    context: Context,
    private var width: Int = ViewGroup.LayoutParams.WRAP_CONTENT
) : AppCompatTextView(context) {

    private var isHeader = false
    private var gridLineColor = Color.GRAY
    private var headerBackgroundColor = Color.parseColor("#3F51B5")
    private var cellBackgroundColor = Color.WHITE

    init {
        initView()
    }

    private fun initView() {
        setBackgroundColor(cellBackgroundColor)
        val params = LinearLayout.LayoutParams(width - 2, ViewGroup.LayoutParams.WRAP_CONTENT)
        params.setMargins(0, 0, 2, 2)
        layoutParams = params
        textSize = 14f
        gravity = Gravity.CENTER
        setPadding(dpToPx(10), dpToPx(10), dpToPx(10), dpToPx(10))
    }

    fun setHeader(isHeader: Boolean) {
        this.isHeader = isHeader
        setBackgroundColor(if (isHeader) headerBackgroundColor else cellBackgroundColor)
        setPadding(
            dpToPx(10), 
            dpToPx(if (isHeader) 15 else 10), 
            dpToPx(10), 
            dpToPx(if (isHeader) 15 else 10)
        )
    }
    
    fun setHeaderBackgroundColor(color: Int) {
        headerBackgroundColor = color
        if (isHeader) {
            setBackgroundColor(color)
        }
    }
    
    fun setCellBackgroundColor(color: Int) {
        cellBackgroundColor = color
        if (!isHeader) {
            setBackgroundColor(color)
        }
    }
    
    fun setGridLineColor(color: Int) {
        gridLineColor = color
        invalidate()
    }

    override fun setWidth(pixels: Int) {
        width = pixels
        refreshWidth()
    }

    private fun refreshWidth() {
        val params = layoutParams as LinearLayout.LayoutParams
        params.width = width - 2
        params.setMargins(0, 0, 2, 2)
        layoutParams = params
    }

    override fun onDraw(canvas: Canvas) {
        // 绘制网格线
        val paint = Paint()
        paint.color = gridLineColor
        paint.strokeWidth = 1f
        
        // 绘制右边框
        canvas.drawLine(width.toFloat() - 2, 0f, width.toFloat() - 2, height.toFloat(), paint)
        // 绘制下边框
        canvas.drawLine(0f, height.toFloat() - 2, width.toFloat(), height.toFloat() - 2, paint)
        
        // 表头特殊样式
        if (isHeader) {
            paint.style = Paint.Style.FILL_AND_STROKE
            paint.strokeWidth = 1.5f
        }
        super.onDraw(canvas)
    }
    
    private fun dpToPx(dp: Int): Int {
        return (dp * context.resources.displayMetrics.density).toInt()
    }
}

在布局文件中添加表格视图

ini 复制代码
 <com.yourpackage.FlexibleTableView
    android:id="@+id/scrollTableView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
相关推荐
sg_knight8 分钟前
Flutter嵌入式开发实战 ——从树莓派到智能家居控制面板,打造工业级交互终端
android·前端·flutter·ios·智能家居·跨平台
Digitally1 小时前
如何轻松将视频从安卓设备传输到电脑?
android·电脑·音视频
Dola_Pan1 小时前
Android四大组件通讯指南:Kotlin版组件茶话会
android·开发语言·kotlin
hopetomorrow2 小时前
学习路之PHP--webman安装及使用
android·学习·php
aningxiaoxixi2 小时前
android 之 Tombstone
android
移动开发者1号2 小时前
应用启动性能优化与黑白屏处理方案
android·kotlin
移动开发者1号2 小时前
Android处理大图防OOM
android·kotlin
张风捷特烈2 小时前
每日一题 Flutter#4 | 说说组件 build 函数的作用
android·flutter·面试
Harrison_zhu5 小时前
在Android13上添加系统服务的好用例子
android
CV资深专家10 小时前
在 Android 框架中,接口的可见性规则
android