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

**表格视图组件,支持:

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

1. 无标题模式支持

设置无标题:调用

Kotlin 复制代码
setHeaderData(null)

Kotlin 复制代码
setHeaderData(emptyList())

自动调整:

隐藏标题行相关视图

智能计算列宽时忽略标题行

保持数据行正常显示和滑动

2. 两种滑动模式

固定第一列模式:

Kotlin 复制代码
 tableView.setScrollMode(FlexibleTableView.ScrollMode.FIXED_FIRST_COLUMN)

第一列垂直固定

标题行和内容区域可水平滚动

适合需要固定标识列的场景

全滑动模式:

Kotlin 复制代码
    tableView.setScrollMode(FlexibleTableView.ScrollMode.FULL_SCROLL)
  • 整个表格可水平滚动

  • 标题行和内容区域同步滚动

  • 适合所有列同等重要的场景

3. 智能列宽计算

等宽模式:

Kotlin 复制代码
    tableView.setEqualColumnWidth(true)
  • 所有列使用相同宽度

  • 宽度取所有列内容最大宽度

自适应模式:

Kotlin 复制代码
 tableView.setEqualColumnWidth(false)

每列根据内容计算宽度

可设置最小宽度保证可读性最小宽度设置:

Kotlin 复制代码
  // 设置第一列最小宽度
    tableView.setFirstColumnMinWidth(120) // 120dp

    // 设置其他列最小宽度
    tableView.setOtherColumnMinWidth(90) // 90dp

4. 全面的样式自定义

Kotlin 复制代码
   //标题行样式:
    tableView.setHeaderTextColor(Color.WHITE)
    tableView.setHeaderBackgroundColor(Color.BLUE)
  
   //第一列样式:
    tableView.setFirstColumnTextColor(Color.DKGRAY)
    tableView.setFirstColumnBackgroundColor(Color.LTGRAY)
 
   //内容区域样式:
    tableView.setContentTextColor(Color.BLACK)
    tableView.setContentBackgroundColor(Color.WHITE)
    
   //网格线样式:
    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
Kotlin 复制代码
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()
    }
}
在布局文件中添加表格视图
Kotlin 复制代码
 <com.yourpackage.FlexibleTableView
    android:id="@+id/scrollTableView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
相关推荐
火柴就是我8 分钟前
Dart 原始字符串(Raw Strings)详解文档
android
玲小珑19 分钟前
Auto.js 入门指南(五)实战项目——自动脚本
android·前端
玲小珑20 分钟前
Auto.js 入门指南(四)Auto.js 基础概念
android·前端
没有了遇见1 小时前
DrawerLayout 滑动冲突
android
玲小珑2 小时前
Auto.js 入门指南(六)多线程与异步操作
android·前端
用户2018792831674 小时前
通俗易懂理解Java注解
android
用户2018792831674 小时前
通俗易懂理解泛型
android
linversion4 小时前
如何手动上传Android混淆映射文件Mapping.txt到Firebase
android
慕晨4 小时前
RecyclerView + SnapHelper 滚动差异问题
android
玲小珑4 小时前
Auto.js 入门指南(三)第一个 Auto.js 脚本
android·前端