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

news2024/11/16 23:55:41

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">

    <LinearLayout
        android:id="@+id/llFreezeColumn"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:showDividers="middle" />


    <View
        android: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.RPWHorizontalScrollView
        android:id="@+id/horScrollView"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:scrollbars="none">

        <LinearLayout
            android: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 {

    @Override
    protected 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 LinearLayoutManager
        for (i in 0..layoutManager.childCount) {
            val v = layoutManager.getChildAt(i)
            if (v != null) {
                val vh = recyclerView.getChildViewHolder(v) as RPWDataGridView<*>.DataGridViewHolder
                vh.rowView.scrollTo(scrollX, 0)
                Log.i(TAG, "alignItems: $horScrollOffset")
            }
        }
        horScrollOffset = scrollX
        headerView.setHorOffset(horScrollOffset)
tHorOffset(horScrollOffset)
//给表头也设置相同的滚动量
    }

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

使用方法

   with(rpwDataGridView) {
            verDividerParams.show = false
            verDividerParams.showHeaderDivider = true
            horDividerParams.show = true
            horDividerParams.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.name
                    rowItemView.cells[1].text = data.password
                    rowItemView.cells[2].text = "11235842364564582"
                    rowItemView.cells[3].text = "2024-04-28"
                    rowItemView.cells[4].text = data.sex
                    rowItemView.cells[5].text = data.phone
                    rowItemView.cells[6].text = data.email
                    rowItemView.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.coloradmin.cn/o/1643131.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

论文辅助笔记:TimeLLM

1 __init__ 2 forward 3 FlattenHead 4 ReprogrammingLayer

Go 语言基础(一)【基本用法】

前言 最近心情格外不舒畅&#xff0c;不仅仅是对前途的迷茫&#xff0c;这种迷茫倒是我自己的问题还好&#xff0c;关键它是我们这种普通吗喽抗衡不了的。 那就换个脑子&#xff0c;学点新东西吧&#xff0c;比如 Go&#xff1f; 1、Go 语言入门 介绍就没必要多说了&#xff0…

vue快速入门(五十四)$nextTick的使用

注释很详细&#xff0c;直接上代码 上一篇 新增内容 $nextTick的使用场景演示 源码 App.vue <template><div id"app"><h3>{{name}}</h3><button click"showfixed">修改</button><form action"post" v-s…

Git常用(持续更新)

常用场景&#xff1a; 初始化&#xff1a; git config --global user.name "codelabs" git config --global user.email mycodelabs.com git init git remote add origin https://github.com/username/repository.git git pull origin master 提交&#xff1a; gi…

设计模式: 责任链模式

目录 一&#xff0c;责任链模式 二&#xff0c;特点 四&#xff0c;实现步骤 五&#xff0c;代码 一&#xff0c;责任链模式 责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种软件设计模式&#xff0c;它属于行为型模式。在这种模式中&#xff0c…

2024五一赛数学建模A题B题C题完整思路+数据代码+参考论文

A题 钢板最优切割路径问题 &#xff08;完整资料在文末获取&#xff09; 1. 建立坐标系和表示方法&#xff1a; 在建模之前&#xff0c;我们需要将切割布局转换为数学表示。首先&#xff0c;我们可以将布局中的每个点表示为二维坐标系中的一个点。例如&#xff0c;B1可以表示…

【吊打面试官系列】Java高并发篇 - Thread 类中的 yield 方法有什么作用?

大家好&#xff0c;我是锋哥。今天分享关于 【Thread 类中的 yield 方法有什么作用&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; Thread 类中的 yield 方法有什么作用&#xff1f; 使当前线程从执行状态&#xff08;运行状态&#xff09;变为可执行态&#x…

【数据结构(邓俊辉)学习笔记】列表04——排序器

文章目录 0. 统一入口1. 选择排序1.1 构思1.2 实例1.3 实现1.4 复杂度 2. 插入排序2.1 构思2.2 实例2.3 实现2.4 复杂度分析2.5 性能分析 3. 归并排序3.1 二路归并算法3.1.1 二路归并算法原理3.1.2 二路归并算法实现3.1.3 归并时间 3.2 分治策略3.2.1 实现3.2.2 排序时间 4. 总…

源支付V7开源版,源支付开源版,已去除安装扩展

源支付V7开源版&#xff0c;源支付开源版&#xff0c;已去除安装扩展 上传源码 设置伪静态 已去除安装扩展&#xff0c;直接上传就可以安装 开源版通道少了好几个 视频教程&#xff1a;https://www.bilibili.com/video/BV1mZ42177VY/

基于OpenCV-DNN的YOLOv9目标检测实现

⚠申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&#xff0c;若要引用&#xff0c;请标注链接地址。 全文共计3077字&#xff0c;阅读大概需要3分钟 &#x1f308;更多学习内容&#xff0c; 欢迎&#x1f44f;关注&#x1f440;【文末】我的个人微信公众号&#xf…

力扣打卡第二天

206. 反转链表 class Solution { public:ListNode* reverseList(ListNode* head) {// //迭代法// ListNode *pre nullptr;// ListNode *curr head;// while(curr){// ListNode *next curr -> next;// curr -> next pre;// pre curr;// curr next;/…

【实时数仓架构】方法论

笔者不是专业的实时数仓架构&#xff0c;这是笔者从其他人经验和网上资料整理而来&#xff0c;仅供参考。写此文章意义&#xff0c;加深对实时数仓理解。 一、实时数仓架构技术演进 1.1 四种架构演进 1&#xff09;离线大数据架构 一种批处理离线数据分析架构&#xff0c;…

SFOS1:开发环境搭建

一、简介 最近在学习sailfish os的应用开发&#xff0c;主要内容是QmlPython。所以&#xff0c;在开发之前需要对开发环境&#xff08;virtualBox官方SDKcmake编译器python&#xff09;进行搭建。值得注意的是&#xff0c;我的开发环境是ubuntu22.04。如果是windows可能大同小异…

ZooKeeper以及DolphinScheduler的用法

目录 一、ZooKeeper的介绍 数据模型 ​编辑 操作使用 ①登录客户端 ​编辑 ②可以查看下面节点有哪些 ③创建新的节点&#xff0c;并指定数据 ④查看节点内的数据 ⑤、删除节点及数据 特殊点&#xff1a; 运行机制&#xff1a; 二、DolphinScheduler的介绍 架构&#…

回溯法——(1)装载问题(C语言讲解)

目录 一、装载问题 1.问题概括&#xff1a; 2.解决方案&#xff08;思路&#xff09;&#xff1a; 3.图片讲解&#xff08;超详细&#xff09;&#xff1a; 4.代码分析&#xff1a; 二、算法改进&#xff1a;引入上界函数 1.问题概念&#xff1a; 2.图片讲解&#xff1a…

Room简单实操

1. Room介绍&#xff0c;直接Copy官网介绍&#xff1a; Room 持久性库在 SQLite 上提供了一个抽象层&#xff0c;以便在充分利用 SQLite 的强大功能的同时&#xff0c;能够流畅地访问数据库。具体来说&#xff0c;Room 具有以下优势&#xff1a; 提供针对 SQL 查询的编译时验…

深入理解分布式事务⑧ ---->MySQL 事务的实现原理 之 MySQL 事务流程(MySQL 事务执行流程 和 恢复流程)详解

目录 MySQL 事务的实现原理 之 MySQL 事务流程&#xff08;MySQL 事务执行流程 和 恢复流程&#xff09;详解MySQL 事务流程1、MySQL 事务执行流程1-1&#xff1a;MySQL 事务执行流程如图&#xff1a; 2、MySQL 事务恢复流程2-1&#xff1a;事务恢复流程如下图&#xff1a; MyS…

基于点灯Blinker的ESP8266远程网络遥控LED

本文介绍基于ESP8266模块实现的远程点灯操作&#xff0c;手机侧APP选用的是点灯-Blinker&#xff0c;完整资料及软件见文末链接 一、ESP8266模块简介 ESP8266是智能家居等物联网场景下常用的数传模块&#xff0c;具有强大的功能&#xff0c;通过串口转WIFI的方式可实现远距离…

区块链扩容:水平扩展 vs.垂直扩展

1. 引言 随着Rollups 的兴起&#xff0c;区块链扩容一直集中在模块化&#xff08;modular&#xff09;vs. 整体式&#xff08;monolithic&#xff09;之争。 如今&#xff0c;模块化与整体式这种一分为二的心理模型&#xff0c;已不适合于当前的扩容场景。本文&#xff0c;将展…

【C语言回顾】字符函数、字符串函数,内存函数

前言1. 字符函数1.1 字符分类函数1.2 字符转换函数1.2.1 tolower&#xff08;将大写字母转化为小写字母&#xff09;1.2.2 toupper&#xff08;将小写字母转化为大写字母&#xff09; 2. 字符串函数2.1 求字符串长度函数 strlen2.2 字符串输入函数 gets()&fgets()2.2.1 get…