题记
Flutter 自带的Table 组件或者是 DataTable 组件,总是不符合预期需求。主要体现在滑动的时候,想要固定表头、或者固定某一列的时候会比较麻烦。然而我们的 toB 项目设计师喜欢设计很多列的表格,emmmm 好吧,封装一个通用点的开源出来😭。
效果图
如图: 可以看到名称这一列和操作这一列是可以固定的,然后中间的数据是可以自由左右滚动的。然后向下滚动的时候标题栏是一直固定在顶部,内容可以正常垂直滚动。
所以实现这么一个表格,需要考虑下面这些点:
- 固定列,同时内容是同时滚动的
- 固定表头,表头不会随着内容垂直滚动,但是左右滚动的时候会跟随(除了固定的列)。
- 自定义表头
- 自定义单元格
- 使用简单,支持各种数据
简单使用
以下是封装后的组件,使用方式。
dart
demo() {
///简单用法
StickyTable(
data: [1, 2, 3, 4, 5],
columns: [
StickyTableColumn(
"Title",
//固定在开头
fixedStart: true,
//固定在结尾
fixedEnd: false,
)
]
);
}
实现思路
Table/ 布局思路
在不考虑滚动联动的情况下,我们布局大致实现可以如下,(伪代码):
dart
Column(
children:[
//单独渲染一次表头
Row(
children:[
//固定的列
if( 存在前面固定的列数据 fixedColumnData)
TableColumn(fixedColumnData),
//可滚动的列
Expanded(
child: SingleChildScrollView(
controller: scrollControl.addAndGet(onlyTitle ? "title" : "body"),
scrollDirection: Axis.horizontal,
physics: const ClampingScrollPhysics(),
child:TableColumn(columnData),
),
),
//固定的列,在后面的
if(存在后面固定的列数据 fixedEndColumnData)
TableColumn(fixedEndColumnData),
]
),
//渲染表格内容
Expanded(
child: SingleChildScrollView(
child: Row(
children:[
//渲染方式应该和上面的表头一致
//固定的列
if( 存在前面固定的列数据 fixedColumnData)
TableColumn(fixedColumnData),
//可滚动的列
Expanded(
child: SingleChildScrollView(
controller: scrollControl.addAndGet(onlyTitle ? "title" : "body"),
scrollDirection: Axis.horizontal,
physics: const ClampingScrollPhysics(),
child:TableColumn(columnData),
),
),
//固定的列,在后面的
if(存在后面固定的列数据 fixedEndColumnData)
TableColumn(fixedEndColumnData),
]
),
),
)
],
)
需要解决问题
上述布局中,可以发现,已经解决了垂直滚动、和固定表头、固定列的逻辑。但是标题栏的可滚动区域和内容的可滚动区域是不同的SingleChildScrollView,所以主要问题就是如何让这两个ScrollView同步滚动。
这里有两个外部库,可任选其一:
我是在这个第三方库的基础上修改了下,以适配自己的组件。
用法很简单,通过以下方式即可绑定不同的ScrollView然后进行同步滚动了。
dart
final _controllers = LinkedScrollControllerGroup();
final _letters = _controllers.addAndGet();
final _numbers = _controllers.addAndGet();
SingleChildScrollView(
controller: _letters.
),
SingleChildScrollView(
controller: _numbers.
),
最后渲染表格
将我们的业务数据转换到 Table 中
less
Table(
children:[
//通过判断是否只渲染表头,来渲染内容
if(onlyRenderTitle)
TableRow(
children:allColumnData.map((e) => Text("标题")).toList()
)
else
// 渲染每一行
for (var row = 0; row < widget.data.length; row++) ...[
TableRow(
children:allColumnData.map((e) => Text("内容")).toList()
)
]
)
以上为实现的核心逻辑,其他的主要通过数据渲染组件的简单逻辑,没什么好讲的了,有兴趣的可以自行去扒拉我的源码,仓库在 git,就一个类文件,下面介绍下封装后的组件如何使用。
GIT 地址: github.com/AlwaysSum/f...
使用
安装
arduino
flutter pub get sticky_table
如何固定表头
less
demo() {
StickyTable(
data: [1, 2, 3, 4, 5],
columns: [
StickyTableColumn(
"Title",
//固定在开头
fixedStart: true,
//固定在结尾
fixedEnd: false,
)
]
);
}
显示斑马线
javascript
demo() {
StickyTable(
showZebraCrossing: true,
zebraCrossingColor: (Colors.white, const Color(0xfff5f5f5)),
zebraCrossingRadius: const Radius.circular(12),
);
}
较为完善的使用示例
less
demo() {
return StickyTable(
// 支持任意数据数组
data: List.generate(50, (index) => sort ? 50 - index : index),
// 默认列宽
defaultColumnWidth: const FixedColumnWidth(80),
// 标题的高度 default: 58
titleHeight: 58,
// 单元格高度 default: 58
cellHeight: 58,
// 每一列的配置
columns: [
StickyTableColumn(
"Title", // 标题 / TITLE
fixedStart: true,
//是否在开头固定 / Is it fixed at the beginning?
showSort: true,
//是否支持排序 /Support sorting
sort: sort,
//排序图标 / Sort i
//列宽:支持百分比和自适应 / Column width, Support percentage and adaptive
columnWidth: const FixedColumnWidth(80),
//单元格的对齐方式 /Align cells
alignment: Alignment.centerLeft,
// 标题的点击时间 / Click time of the title
onTitleClick: (context, title) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text("Sort")));
setState(() {
sort = !(title.sort ?? false);
});
},
// 自定义渲染单元格 / Custom rendering cells
renderCell: (context, title, data, row, column) {
return Text("Name$data");
},
//自定义渲染标题 / Custom rendering title
renderTitle: (context, title) {
return Text(
title.title,
style: const TextStyle(color: Colors.purple),
);
},
),
StickyTableColumn(
"Age",
showSort: true,
sort: false,
onCellClick: (context, title, data, row, column) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("年龄$data")));
},
renderCell: (context, title, data, row, column) {
return Text("*$data");
},
),
...List.generate(
10,
(index) =>
StickyTableColumn(
"Sub Title $index",
showSort: true,
sort: false,
renderCell: (context, title, data, row, column) {
return Text("Content $row-$column");
},
),
),
StickyTableColumn(
"Option",
// 固定在结尾
fixedEnd: true,
columnWidth: const FixedColumnWidth(100),
renderCell: (context, title, data, row, column) {
return MaterialButton(
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("删除$data成功")));
},
color: Colors.red,
minWidth: 0,
child: const Text(
"Delete",
style: TextStyle(color: Colors.white),
),
);
},
),
],
);
}