动手写一个简单的Android 表格控件支持固定列

ops/2025/2/22 1:20:18/

Android 动手写一个简洁版表格控件

简介

源码已放到
Github
Gitee

作为在测绘地理信息行业中穿梭的打工人,遇到各种数据采集需求,既然有数据采集需求,那当然少不了数据展示功能,最常见的如表格方式展示。
当然,类似表格这些控件网上也有挺多开源的,但是经过我一番思考,决定自己动手撸一个,还能了解下原理。

实现思路

如下图所示,我们把表格拆分成三部分,表头、固定列、表格内容,其中固定列顾名思义,位置固定,内容部分,当宽度超过可视范围时,可左右滚动
表格结构
对于表格垂直方向的滚动,我们可以用Rrecyclerview 来实现,那么水平方向的滚动,我们可以使用HorizontalScrollerView,
这样我们就可以得到一个初步的表格雏形,对应类暂且叫RPWDataGridView
行设计

关键属性、接口代码:

class RPWDataGridView<T> @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {private val headerView: RPWDataGridIRowItemView//表头private val recyclerView: RecyclerView//表格内容private val columns = mutableListOf<RPWDataGridColumn>()//列参数,每一行共用同一列参数,保证每个单元格的宽度一致private var horScrollOffset = 0//当前水平滚动偏移量,保证每一行滚动量一致private val dataSource = mutableListOf<T>()//数据源private var dataGridAdapter = DataGridAdapter() //数据适配器fun build(vararg columns: RPWDataGridColumn) {//构建表格结构//...}/*** 设置表格数据源*/fun setDataSource(data: List<T>) {//...}
}

众所周知,每一行里面又会按列分成狠多单元格,所以我们还得再把HorizontalScrollerView按列细分,里面单元格通过动态添加TextView来实现,由于需要固定列,所以为了方便实现固定的逻辑,我们做如下设计:

在这里插入图片描述
然后封装一个表格的行控件,暂且命名为RPWDataGridIRowItemView,该控件View的结构如上图所示,固定列使用一个LinearLayout ,滚动列使用HorizontalScrollerView, 代码层面,伪代码:

  1. View层:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/llRoot"android:layout_width="match_parent"android:layout_height="@dimen/ui_data_grid_row_min_height"android:background="@drawable/data_grid_view_row_item_background"android:clickable="true"android:focusable="true"android:focusableInTouchMode="true"android:orientation="horizontal"><LinearLayoutandroid:id="@+id/llFreezeColumn"android:layout_width="wrap_content"android:layout_height="match_parent"android:orientation="horizontal"android:showDividers="middle" /><Viewandroid:id="@+id/viewVerHeaderDivider"android:background="@color/ui_data_grid_header_divider_color"android:layout_width="@dimen/ui_data_grid_header_divider_size"android:layout_height="match_parent"/><com.rpw.view.RPWHorizontalScrollViewandroid:id="@+id/horScrollView"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:scrollbars="none"><LinearLayoutandroid:id="@+id/llScrollColumn"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="horizontal"android:showDividers="middle" /></com.rpw.view.RPWHorizontalScrollView>
</LinearLayout>
  1. 代码层
class RPWDataGridIRowItemView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {//冻结列父布局private val llFreezeColumn: LinearLayout//滚动列父布局private val llScrollColumn: LinearLayout//RPWDataGridColumn为列参数fun addColumn(column: RPWDataGridView.RPWDataGridColumn) {if (column.freeze) {llFreezeColumn.addView(TextView())}else{llScrollColumn.addView(TextView())}}}

然后把他作为RecyclerViewItemView 加载到每一行中。
那么问题来了,每一行都有自己的滚动View,各滚各的,这跟表格也不一样。
所以,为了解决这个问题,我们需要给每个HorizontalScrollerView 注册滚动监听,当某个HorizontalScrollerView 发生滚动,我们把其他的HorizontalScrollerView 也设置同样的滚动量不就可以对齐了吗。
是的,但是在实现这个逻辑前,由于他不对外暴露滚动状态,我们还得继承HorizontalScrollerView 重写 onScrollChanged 函数,暂且命名为RPWHorizontalScrollView,我们专属的水平滚动View。
国际惯例,上关键代码:

public class RPWHorizontalScrollView extends HorizontalScrollView {@Overrideprotected void onScrollChanged(int l, int t, int oldl, int oldt) {super.onScrollChanged(l, t, oldl, oldt);if (null != listener)listener.onCustomScrollChange(RPWHorizontalScrollView.this, l, t, oldl, oldt);
//通知滚动变化}}

接下来,我们还需要补充一下对齐RecyclerView 中所有已加载的ItemView ,这个代码需要写到表格控件RPWDataGridView 中,与其他行共享同一偏移量,对齐关键代码如下:

/*** 对齐当前视图下每一行的滚动偏移*/private fun alignItems(scrollX: Int) {val layoutManager = recyclerView.layoutManager as LinearLayoutManagerfor (i in 0..layoutManager.childCount) {val v = layoutManager.getChildAt(i)if (v != null) {val vh = recyclerView.getChildViewHolder(v) as RPWDataGridView<*>.DataGridViewHoldervh.rowView.scrollTo(scrollX, 0)Log.i(TAG, "alignItems: $horScrollOffset")}}horScrollOffset = scrollXheaderView.setHorOffset(horScrollOffset)
tHorOffset(horScrollOffset)
//给表头也设置相同的滚动量}

然后在适配器中监听和绑定每一行的滚动量,给他设置到全局horScrollOffset 中,在适配器onBindViewHolder 的时候,给他设置这个偏移量,实现新的行也对齐。
完整封装的代码就不在这里详细展示了,有兴趣可以到Gitee上查看

使用方法

   with(rpwDataGridView) {verDividerParams.show = falseverDividerParams.showHeaderDivider = truehorDividerParams.show = truehorDividerParams.showHeaderDivider = true//region build column//构建表格结构build(RPWDataGridView.RPWDataGridColumn(DensityUtil.dpToPx(this@MainActivity, 100), "姓名", true),RPWDataGridView.RPWDataGridColumn(DensityUtil.dpToPx(this@MainActivity, 100), "密码", false),RPWDataGridView.RPWDataGridColumn(DensityUtil.dpToPx(this@MainActivity, 200), "身份证号码", false),RPWDataGridView.RPWDataGridColumn(DensityUtil.dpToPx(this@MainActivity, 200), "出生年月", false),RPWDataGridView.RPWDataGridColumn(DensityUtil.dpToPx(this@MainActivity, 60), "性别", false),RPWDataGridView.RPWDataGridColumn(DensityUtil.dpToPx(this@MainActivity, 150), "手机号码", false),RPWDataGridView.RPWDataGridColumn(DensityUtil.dpToPx(this@MainActivity, 150), "邮箱", false),RPWDataGridView.RPWDataGridColumn(DensityUtil.dpToPx(this@MainActivity, 300), "地址", false),)//endregion//绑定每行显示的数据setRowBuildListener(object : RPWDataGridView.RowBuildListener<ItemData> {override fun onBuildRow(rowItemView: RPWDataGridIRowItemView, data: ItemData) {rowItemView.cells[0].text = data.namerowItemView.cells[1].text = data.passwordrowItemView.cells[2].text = "11235842364564582"rowItemView.cells[3].text = "2024-04-28"rowItemView.cells[4].text = data.sexrowItemView.cells[5].text = data.phonerowItemView.cells[6].text = data.emailrowItemView.cells[7].text = data.address}})//监听单元格点击setRowClickListener(object : RPWDataGridView.RowClickListener<ItemData> {override fun onRowClick(data: ItemData, rowIndex: Int, columnIndex: Int) {Toast.makeText(this@MainActivity, "点击坐标[$rowIndex:$columnIndex]", Toast.LENGTH_SHORT).show()}override fun onRowLongClick(t: ItemData, rowIndex: Int, columnIndex: Any?): Boolean {rpwDataGridView.startSelect(true)return true}})//监听页面状态变化setStatusListener(object : RPWDataGridView.DataGridViewStatusListener {override fun onStatusChange(statusEnum: RPWDataGridViewStatusEnum) {Toast.makeText(this@MainActivity, "状态改变:$statusEnum", Toast.LENGTH_SHORT).show()}})val ds = mutableListOf<ItemData>()repeat(1000) {//添加1000条测试数据ds.add(ItemData("WPR$it",it.toString(),"$it","广东省广州市番禺区xxxxxx$it 号","123456789"))}setDataSource(ds)}

嗯嗯嗯~~按照这个思路,实现如下:
实现效果

总结

至此,简单的表格效果已有,目前发现有一些UI体验层面的bug,后面我会看情况在Gitee中完善,因为是想写一个简单易用的表格控件,所以对每个单元格里面的View都写死成TextView了,另一方面是我需求没那么复杂。。


http://www.ppmy.cn/ops/35469.html

相关文章

flask sqlalchemy 多条数据删除

flasksqlalchemy 多条数据删除 在Flask-SQLAlchemy中&#xff0c;如果你想要删除多条数据&#xff0c;可以使用delete()方法配合filter()来指定条件。以下是一个删除满足特定条件的所有记录的例子&#xff1a; from flask_sqlalchemy import SQLAlchemy from your_flask_app i…

数据库优化

一、主从读写分离 主库:主要负责数据的写入。 从库:主要负责数据的查询。 引出问题: 可能会存在主从延迟,导致主从一致性问题。查询主库的量级需要控制。数据量庞大,索引也占据存储空间,磁盘空间不足,当主库宕机后会影响所有模块的写入,需要进行数据分片,因此引出分库…

【机器学习与实现】线性回归分析

目录 一、相关和回归的概念&#xff08;一&#xff09;变量间的关系&#xff08;二&#xff09;Pearson&#xff08;皮尔逊&#xff09;相关系数 二、线性回归的概念和方程&#xff08;一&#xff09;回归分析概述&#xff08;二&#xff09;线性回归方程 三、线性回归模型的损…

Unity 性能优化之图片优化(八)

提示&#xff1a;仅供参考&#xff0c;有误之处&#xff0c;麻烦大佬指出&#xff0c;不胜感激&#xff01; 文章目录 前言一、可以提前和美术商量的事1.避免内存浪费&#xff08;UI图片&#xff0c;不是贴图&#xff09;2.提升图片性能 二、图片优化1.图片Max Size修改&#x…

19_Scala集合概述

文章目录 集合回顾javaScala集合三大类String & StringBuilderScala集合两大类 集合 回顾java scala与Java有所不同 函数式编程语言更侧重集合本身提供的哪些功能&#xff1b; Scala集合三大类 1.Seq 存储有序数据可重复 类比 List 2.Set 存储无序数据不可重复 3.Map…

Verilog中使用generate…for语句简化代码

在某个module中包含了很多相似的连续赋值语句&#xff0c;请使用generata…for语句编写代码&#xff0c;替代该语句&#xff0c;要求不能改变原module的功能。 原代码如下&#xff1a; &#xff08;CSDN代码块不支持Verilog&#xff0c;代码复制到notepad编辑器中&#xff0c…

centos无法tab补全至文件

很奇怪的需求&#xff1a;redhat 7.9版本用cd 只能到目录&#xff0c;无法到文件 我个人认为不是个问题&#xff0c;但是甲方需求&#xff0c;你懂的 首先&#xff0c;我们要搞清楚tab补全功能的包bash-completion是否安装&#xff0c;这里肯定是安装了&#xff0c;不过还是看…

【Git】【MacOS】Github从创建与生成SSH公钥

创建账号 这一步不过多赘述&#xff0c;根据自己的邮箱新创建一个账号 配置SSH公钥 本人是macOS系统&#xff0c;首先从终端输入 cd ~/.ssh进入.ssh目录,然后通过 ls查看有没有一个叫做id_rsa.pub的文件 本人之前生成过SSH公钥,如果没有的话&#xff0c;通过 ssh-keygen -t…