如图,想要实现这种表格样式,如何实现呢?直接上代码喽...
1.使用DataTable
int rowsCount = 100;
int columnsCount = 30;
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: DataTable(
columns: columns(),
rows: rows(),
),
),
);
List columns() {
List column = [];
for (var i = 0; i < columnsCount; i++) {
column.add(DataColumn(label: Text('列标题$i')));
}
return column;
}
List rows() {
//行
List rows = [];
for (var i = 0; i < rowsCount; i++) {
rows.add(DataRow(cells: cells()));
}
return rows;
}
List cells() {
List cells = [];
for (var i = 0; i < columnsCount; i++) {
cells.add(DataCell(Text('$i')));
}
return cells;
}
如上,即可实现一个表格,且可以上下左右滚动,但是顶部行 、左侧列无法固定, 这可怎么办呢? 我们可以在顶部、左侧加入列表,实现同步滚动不就可以了嘛
参考 https://stackoom.com/question/3x2Bv
此文章给出例子是在顶部、左侧加入了一个DataTable,但是同步滚动仅实现了滚动内部表格,左侧及顶部列表的同步滚动,并没有实现滚动左侧及顶部列表,表格跟随滚动的效果;
1.
- 于是,我们结合linked_scroll_controller库,修改成这个样子
class CustomDataTable extends StatefulWidget {
final T? fixedCornerCell;
final List? fixedColCells;
final List? fixedRowCells;
final List>? rowsCells;
final Widget Function(T data)? cellBuilder;
final double fixedColWidth;
final double cellWidth;
final double cellHeight;
final double cellMargin;
final double cellSpacing;
CustomDataTable({
this.fixedCornerCell,
this.fixedColCells,
this.fixedRowCells,
@required this.rowsCells,
this.cellBuilder,
this.fixedColWidth = 60.0,
this.cellHeight = 56.0,
this.cellWidth = 120.0,
this.cellMargin = 10.0,
this.cellSpacing = 10.0,
});
@override
State createState() => CustomDataTableState();
}
class CustomDataTableState extends State> {
ScrollController? _columnController;
ScrollController? _rowController;
ScrollController? _subTableYController;
ScrollController? _subTableXController;
LinkedScrollControllerGroup? _verticalControllers;
LinkedScrollControllerGroup? _horizontalControllers;
@override
void initState() {
super.initState();
_verticalControllers = LinkedScrollControllerGroup();
_horizontalControllers = LinkedScrollControllerGroup();
_columnController = _verticalControllers?.addAndGet();
_subTableYController = _verticalControllers?.addAndGet();
_rowController = _horizontalControllers?.addAndGet();
_subTableXController = _horizontalControllers?.addAndGet();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Row(
children: [
SingleChildScrollView(
controller: _columnController,
scrollDirection: Axis.vertical,
// physics: NeverScrollableScrollPhysics(),
child: _buildFixedCol(), //左侧列表
),
Flexible(
child: SingleChildScrollView(
controller: _subTableXController,
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
controller: _subTableYController,
scrollDirection: Axis.vertical,
child: _buildSubTable(),//中间表格
),
),
),
],
),
Row(
children: [
_buildCornerCell(),//左上角单元格
Flexible(
child: SingleChildScrollView(
controller: _rowController,
scrollDirection: Axis.horizontal,
// physics: NeverScrollableScrollPhysics(),
child: _buildFixedRow(),//顶部列表
),
),
],
),
],
);
}
Widget _buildChild(double width, T? data) {//单元格元素
if (data == null) {
return SizedBox(width: width, child: Text(''));
}
return SizedBox(
width: width, child: widget.cellBuilder?.call(data) ?? Text('$data'));
}
//左侧列表
Widget _buildFixedCol() => widget.fixedColCells == null
? SizedBox.shrink()
: Material(
color: Colors.blueGrey[100],
child: DataTable(
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: [
DataColumn(
label: _buildChild(
widget.fixedColWidth, widget.fixedColCells?.first))
],//顶部重复第一个元素作为标题,会被左上角单元格覆盖
rows: widget.fixedColCells!
.sublist(widget.fixedRowCells == null ? 1 : 0)
.map((c) => DataRow(
cells: [DataCell(_buildChild(widget.fixedColWidth, c))]))
.toList()),
);
//顶部列表
Widget _buildFixedRow() => widget.fixedRowCells == null
? SizedBox.shrink()
: Material(
color: Colors.blueGrey[100],
child: DataTable(
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: widget.fixedRowCells!
.map((c) =>
DataColumn(label: _buildChild(widget.cellWidth, c)))
.toList(),
rows: []),
);
//表格
Widget _buildSubTable() => Material(
color: Colors.white,
child: DataTable(
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: widget.rowsCells!.first
.map((c) => DataColumn(label: _buildChild(widget.cellWidth, c)))
.toList(),//顶部重复第一个元素作为标题,会被顶部横向列表覆盖
rows: widget.rowsCells!
.sublist(widget.fixedRowCells == null ? 1 : 0)
.map((row) => DataRow(
cells: row
.map((c) => DataCell(_buildChild(widget.cellWidth, c)))
.toList()))
.toList()));
//左上角单元格
Widget _buildCornerCell() =>
widget.fixedColCells == null || widget.fixedRowCells == null
? SizedBox.shrink()
: Material(
color: Colors.blueGrey[100],
child: DataTable(
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: [
DataColumn(
label: _buildChild(
widget.fixedColWidth, widget.fixedCornerCell))
],
rows: []),
);
}
- 调用显示
final _rowsCells = [
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8],
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8],
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8],
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8]
];
final _fixedColCells = [
"Pablo",
"Gustavo",
"John",
"Jack",
"Pablo",
"Gustavo",
"John",
"Jack",
"Pablo",
"Gustavo",
"John",
"Jack",
"Pablo",
"Gustavo",
"John",
"Jack",
];
final _fixedRowCells = [
"Math",
"Informatics",
"Geography",
"Physics",
"Biology"
];
return CustomDataTable(
rowsCells: _rowsCells,//表格内单元格数据
fixedColCells: _fixedColCells,//左侧标题数据
fixedRowCells: _fixedRowCells,//顶部标题数据
cellBuilder: (data) {
return Center(//单元格内容
child: Text('$data', style: TextStyle(color: Colors.black)));
},
);
2.
- 由上可实现固定标题的可滚动表格,但是我们可以发现,左侧及顶部列表也是使用表格实现,似乎不是那么必要,可修改为ListView实现
class CustomDataTable extends StatefulWidget {
final T? fixedCornerCell;
final List? fixedColCells;
final List? fixedRowCells;
final List>? rowsCells;
final Widget Function(T data)? cellBuilder;
final double fixedColWidth;
final double cellWidth;
final double cellHeight;
final double cellMargin;
final double cellSpacing;
CustomDataTable({
this.fixedCornerCell,
this.fixedColCells,
this.fixedRowCells,
@required this.rowsCells,
this.cellBuilder,
this.fixedColWidth = 60.0,
this.cellHeight = 56.0,
this.cellWidth = 120.0,
this.cellMargin = 10.0,
this.cellSpacing = 10.0,
});
@override
State createState() => CustomDataTableState();
}
class CustomDataTableState extends State> {
ScrollController? _columnController;
ScrollController? _rowController;
ScrollController? _subTableYController;
ScrollController? _subTableXController;
LinkedScrollControllerGroup? _verticalControllers;
LinkedScrollControllerGroup? _horizontalControllers;
@override
void initState() {
super.initState();
_verticalControllers = LinkedScrollControllerGroup();
_horizontalControllers = LinkedScrollControllerGroup();
_columnController = _verticalControllers?.addAndGet();
_subTableYController = _verticalControllers?.addAndGet();
_rowController = _horizontalControllers?.addAndGet();
_subTableXController = _horizontalControllers?.addAndGet();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
bottom: 0,
child: Row(
children: [
Container(
width: widget.fixedColWidth,
child: ListView.builder(
controller: _columnController,
itemBuilder: (cobtext, index) {
if (index == 0) {
return Container(
height: widget.cellHeight,
child: _buildChild(widget.fixedColWidth,
widget.fixedColCells?.first),
);
}
return Container(
color: Colors.blueGrey[100],
height: widget.cellHeight,
child: _buildChild(widget.fixedColWidth,
widget.fixedColCells?[index - 1]),
);
},
itemCount: (widget.fixedColCells?.length ?? 0) == 0
? 0
: (widget.fixedColCells?.length ?? 0) + 1),
),
Flexible(
child: SingleChildScrollView(
controller: _subTableXController,
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
controller: _subTableYController,
scrollDirection: Axis.vertical,
child: _buildSubTable(), //中间表格
),
),
),
],
),
),
Positioned(
top: 0,
left: 0,
right: 0,
height: widget.cellHeight,
child: Row(
children: [
// _buildCornerCell(),
Container(
color: Colors.blueGrey[100],
width: widget.fixedColWidth,
height: widget.cellHeight,
),
Flexible(
child: Container(
color: Colors.blueGrey[100],
child: ListView.builder(
padding: EdgeInsets.only(
right: widget.cellMargin,
),
scrollDirection: Axis.horizontal,
controller: _rowController,
itemBuilder: (context, index) {
return Container(
width: widget.cellWidth,
margin: EdgeInsets.only(
left: widget.cellMargin,
),
height: widget.cellHeight,
child: _buildChild(
widget.fixedColWidth, widget.fixedRowCells?[index]),
);
},
itemCount: widget.fixedRowCells?.length),
)),
],
),
),
],
);
}
Widget _buildChild(double width, T? data) {
if (data == null) {
return SizedBox();
}
return SizedBox(
width: width, child: widget.cellBuilder?.call(data) ?? Text('$data'));
}
//中间表格
Widget _buildSubTable() => Material(
color: Colors.white,
child: DataTable(
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: widget.rowsCells!.first
.map((c) => DataColumn(label: _buildChild(widget.cellWidth, c)))
.toList(),
rows: widget.rowsCells!
.sublist(widget.fixedRowCells == null ? 1 : 0)
.map((row) => DataRow(
cells: row
.map((c) => DataCell(_buildChild(widget.cellWidth, c)))
.toList()))
.toList()));
}
2. ListView+linked_scroll_controller库
- 不使用表格,完全使用ListView的嵌套及同步滚动完成
linked_scroll_controller 库: 可实现列表的同步滚动
附上完整代码
class TimeTablePage extends StatefulWidget {
const TimeTablePage({Key? key}) : super(key: key);
@override
_TimeTablePageState createState() => _TimeTablePageState();
}
class _TimeTablePageState extends State {
int rowsCount = 100;
int columnsCount = 30;
double cellWidth = 80;
double cellHeight = 50;
ScrollController? leftController;
ScrollController? topController;
ScrollController? rowController;
List columnsController = [];
LinkedScrollControllerGroup? _verticalControllers;
LinkedScrollControllerGroup? _horizontalControllers;
@override
void initState() {
super.initState();
_verticalControllers = LinkedScrollControllerGroup();
_horizontalControllers = LinkedScrollControllerGroup();
leftController = _verticalControllers?.addAndGet();
topController = _horizontalControllers?.addAndGet();
rowController = _horizontalControllers?.addAndGet();
for (var i = 0; i < columnsCount; i++) {
ScrollController? columnController = _verticalControllers?.addAndGet();
if (columnController != null) {
columnsController.add(columnController);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("工时表"),
),
body: _bodyWidget(),
);
}
Widget _bodyWidget() {
return Row(children: [
Container(
width: cellWidth,
child: Column(children: [
Container(
decoration: BoxDecoration(
color: Colors.blueGrey[100],
border: Border.all(
color: ColorUtils.parseColorString("eeeeee"), width: 1)),
padding: EdgeInsets.only(left: 5.0, right: 5.0),
height: cellHeight,
width: cellWidth,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
alignment: Alignment.topRight,
child: Text("时间"),
),
Container(
alignment: Alignment.bottomLeft, child: Text("姓名"))
])),
Expanded(
child: ListView.builder(
controller: leftController,
scrollDirection: Axis.vertical,
padding: EdgeInsets.all(0),
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.blueGrey[100],
border: Border.all(
color: ColorUtils.parseColorString("eeeeee"),
width: 1)),
height: cellHeight,
child: Center(child: Text("张${index + 1}")));
},
itemCount: rowsCount - 1,
),
),
]),
),
Expanded(
child: Column(children: [
Container(
height: cellHeight,
child: ListView.builder(
controller: topController,
padding: EdgeInsets.all(0),
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.blueGrey[100],
border: Border.all(
color: ColorUtils.parseColorString("eeeeee"),
width: 1)),
width: cellWidth,
child: Center(child: Text("8月${index + 1}日")));
},
itemCount: columnsCount,
),
),
Expanded(
child: ListView.builder(
controller: rowController,
padding: EdgeInsets.all(0),
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Container(
width: cellWidth,
child: ListView.builder(
controller: columnsController[index],
scrollDirection: Axis.vertical,
shrinkWrap: true,
padding: EdgeInsets.all(0),
itemBuilder: (context, innerindex) {
return Container(
height: cellHeight,
child: Center(child: Text("$innerindex, $index")));
},
itemCount: rowsCount - 1,
),
);
},
itemCount: columnsCount,
))
]))
]);
}
@override
void dispose() {
super.dispose();
leftController?.dispose();
topController?.dispose();
rowController?.dispose();
for (var controller in columnsController) {
controller.dispose();
}
}
}