该项目为flutter表格的初级版,后续更完善的版本已发布文章:Flutter用700行代码纯手工自定义绘制表格控件KqTablehttps://blog.csdn.net/u012800952/article/details/129549252?spm=1001.2014.3001.5502
- 演示
- 功能
1.支持动态数据绘制。数据格式[[row],[row],...]。
2.支持表格中文字的大小与颜色设置。
3.支持控件宽高设置。
4.支持设置表格的格子背景颜色与边框颜色。
5.支持固定上下左右行列,固定遵循格式,代表上下左右固定的行与列数:[int,int,int,int]
6.支持指定任意行列的颜色,设置格式:[TableColor,TableColor,...],TableColor有两个子函数。设置行颜色的RowColor与设置列颜色的RowColor。
7.支持单元格点击回调,回调会返回所点击的单元格的对应数据对象T。
8.支持列宽度拖拽,在第一行位置按住列分隔线便可拖拽列宽度。
- 代码
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
class KqTable<T> extends StatefulWidget {
/// 控件数据
final List<List<T>> data;
/// 文本大小
final double fontSize;
/// 文本颜色
final Color textColor;
/// 控件宽度
final double width;
/// 控件高度
final double height;
/// 表格颜色
final Color tableColor;
/// 表格边框颜色
final Color tableBorderColor;
/// 上下左右固定行数值[int,int,int,int]
final List<int> lockList;
/// 指定特定行或者列的颜色,行使用[RowColor],列使用[ColumnColor]
final List<TableColor> colorList;
/// 点击单元格回调
final Function(T data)? onTap;
const KqTable(
{super.key,
required this.data,
this.fontSize = 14,
this.textColor = Colors.black,
this.width = 300,
this.height = 200,
this.tableColor = Colors.white,
this.tableBorderColor = Colors.blueAccent,
this.lockList = const [1, 0, 1, 0],
this.colorList = const [
RowColor(0, Colors.grey),
RowColor(5, Colors.grey),
ColumnColor(0, Colors.grey),
ColumnColor(6, Colors.grey)
],
this.onTap});
@override
State<StatefulWidget> createState() => _KqTableState<T>();
}
class _KqTableState<T> extends State<KqTable<T>> {
///边线判断误差,用于判定拖拽列宽时是否点击在列线上的判定
final _columnWidthOffset = 4;
/// x方向偏移量
double _offsetDx = 0;
/// y方向偏移量
double _offsetDy = 0;
/// x方向误差量
double _diffOffsetDx = 0;
/// y方向误差量
double _diffOffsetDy = 0;
/// 行数
int _rowLength = 0;
/// 列数
int _columnLength = 0;
/// 每列的行文本最大宽度列表[[原宽度,调整后宽度],[原宽度,调整后宽度],...]
final List<List<double>> _rowWidthList = [];
double _columnHeight = 0;
/// 表格总宽度
double _tableWidth = 0;
/// 表格总高度
double _tableHeight = 0;
/// 按下时当前单元格的对象
T? _operateTableData;
/// 当前手势是否滑动
bool _operateIsMove = false;
/// 当前是否处于拖拽状态
bool _operateIsDrag = false;
/// 当前正在拖拽第几列的边线
int _operateColumnLineIndex = 0;
/// 当前正在拖拽边线前面累加的宽度值
double _operateTotalWidth = 0;
@override
void initState() {
super.initState();
_initData();
}
void _initData() {
_rowLength = widget.data[0].length;
_columnLength = widget.data.length;
for (int i = 0; i < _rowLength; i++) {
double maxWidth = 0;
for (int j = 0; j < _columnLength; j++) {
String str = widget.data[j][i] as String;
TextPainter textPainter = TextPainter(
text: TextSpan(
text: str,
style: TextStyle(
color: Colors.redAccent, fontSize: widget.fontSize)),
maxLines: 1,
textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: double.infinity);
if (maxWidth < textPainter.width) {
maxWidth = textPainter.width;
}
_columnHeight = textPainter.height;
}
_rowWidthList.add([maxWidth, maxWidth]);
_tableWidth += maxWidth;
}
_tableHeight = _columnHeight * _columnLength;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onVerticalDragUpdate: (_) {},
child: RepaintBoundary(
child: SizedBox(
width: widget.width,
height: widget.height,
child: ClipRect(
child: Listener(
child: CustomPaint(
painter: _TablePainter(this, _offsetDx, _offsetDy, widget.data,
_rowLength, _columnLength, _rowWidthList, _columnHeight),
),
onPointerDown: (PointerDownEvent event) {
_operateIsMove = false;
_operateIsDrag = false;
//事件点击的中心位置
Offset? eventOffset = event.localPosition;
_diffOffsetDx = eventOffset.dx - _offsetDx;
_diffOffsetDy = eventOffset.dy - _offsetDy;
///判定按下在哪个单元格,并获取单元格内容
//点击的横向坐标
int r = 0;
//点击的纵向坐标
int c = 0;
if (eventOffset.dy - widget.lockList[0] * _columnHeight < 0) {}
//计算横向坐标
List<double> totalReversalRowWidthList = [];
double totalReversalRowWidth = 0;
for (int i = _rowLength - 1; i >= 0; i--) {
totalReversalRowWidth += _rowWidthList[i][1];
totalReversalRowWidthList.add(totalReversalRowWidth);
}
double totalWidth = 0;
for (int i = 0; i < _rowWidthList.length; i++) {
totalWidth += _rowWidthList[i][1];
if (i < widget.lockList[2]) {
//左
if (eventOffset.dx.abs() - totalWidth < 0) {
c = i;
break;
}
} else {
//中间
if ((eventOffset.dx - _offsetDx).abs() - totalWidth < 0) {
c = i;
break;
}
}
}
//右
if (widget.lockList[3] != 0 &&
eventOffset.dx -
(widget.width -
totalReversalRowWidthList[
widget.lockList[3] - 1]) >
0) {
for (int i = 0; i < totalReversalRowWidthList.length; i++) {
if (eventOffset.dx -
(widget.width - totalReversalRowWidthList[i]) >
0) {
c = _rowLength - i - 1;
break;
}
}
}
//计算纵向坐标
if (eventOffset.dy - widget.lockList[0] * _columnHeight < 0) {
//上
r = (eventOffset.dy).abs() ~/ _columnHeight;
} else if (eventOffset.dy -
(widget.height - widget.lockList[1] * _columnHeight) >
0) {
//下
r = _columnLength -
(widget.lockList[1] -
(eventOffset.dy -
(widget.height -
widget.lockList[1] * _columnHeight))
.abs() ~/
_columnHeight);
} else {
//中间
r = (eventOffset.dy - _offsetDy).abs() ~/ _columnHeight;
}
//获取坐标对应的值
_operateTableData = widget.data[r][c];
/// 判断列宽度拖拽
//点击在第一行行高位置的row的边线上便认定为正在拖拽列宽度
_operateTotalWidth = 0;
totalWidth = 0;
for (int i = 0; i < _rowWidthList.length; i++) {
totalWidth += _rowWidthList[i][1];
if (((eventOffset.dx - _offsetDx).abs() - totalWidth).abs() <
_columnWidthOffset &&
(eventOffset.dy - _offsetDy).abs() < _columnHeight) {
_operateIsDrag = true;
_operateColumnLineIndex = i;
break;
}
_operateTotalWidth += _rowWidthList[i][1];
}
},
onPointerMove: (PointerMoveEvent event) {
_operateIsMove = true;
//事件点击的中心位置
Offset? eventOffset = event.localPosition;
if (!_operateIsDrag) {
//表格移动
setState(() {
_offsetDx = eventOffset.dx - _diffOffsetDx;
_offsetDy = eventOffset.dy - _diffOffsetDy;
// 边界处理
// 当有固定行时
// 上边限定
if (_offsetDy >= 0) {
_offsetDy = 0;
}
// 左边限定
if (_offsetDx >= 0) {
_offsetDx = 0;
}
// 右边限定
double rightOffset = 0;
for (int i = 0; i < widget.lockList[3]; i++) {
rightOffset +=
_rowWidthList[_rowWidthList.length - i - 1][1];
}
if (_offsetDx <= (widget.width + rightOffset) - _tableWidth) {
_offsetDx = (widget.width + rightOffset) - _tableWidth;
}
// 下边限定
if (_offsetDy <=
(widget.height + widget.lockList[1] * _columnHeight) -
_tableHeight) {
_offsetDy =
(widget.height + widget.lockList[1] * _columnHeight) -
_tableHeight;
}
//当表格宽度小于控件宽度,则不能水平移动
if (_tableWidth <= widget.width) {
_offsetDx = 0;
}
//当表格高度小于控件高度,则不能上下移动
if (_tableHeight <= widget.height) {
_offsetDy = 0;
}
});
} else {
//表格row拖拽
setState(() {
if (eventOffset.dx - _operateTotalWidth >=
_rowWidthList[_operateColumnLineIndex][0]) {
_rowWidthList[_operateColumnLineIndex][1] =
eventOffset.dx - _operateTotalWidth;
} else {
_rowWidthList[_operateColumnLineIndex][1] =
_rowWidthList[_operateColumnLineIndex][0];
}
});
}
},
onPointerUp: (PointerUpEvent event) {
//判定没有滑动则为点击
if (!_operateIsMove) {
widget.onTap?.call(_operateTableData as T);
}
},
)),
)));
}
}
class _TablePainter<T> extends CustomPainter {
/// state
final _KqTableState state;
/// x方向偏移量
final double _offsetDx;
/// y方向偏移量
final double _offsetDy;
final List<List<T>>? _data;
/// 行数
final int _rowLength;
/// 列数
final int _columnLength;
/// 每列的行文本最大宽度列表
final List<List<double>> _rowWidthList;
/// 每行的文本高度
final double _columnHeight;
_TablePainter(
this.state,
this._offsetDx,
this._offsetDy,
this._data,
this._rowLength,
this._columnLength,
this._rowWidthList,
this._columnHeight);
@override
void paint(Canvas canvas, Size size) {
//表格边框画笔
final Paint paint1 = Paint()
..strokeCap = StrokeCap.square
..isAntiAlias = true
..style = PaintingStyle.stroke
..color = state.widget.tableBorderColor;
//表格背景画笔
final Paint paint2 = Paint()
..strokeCap = StrokeCap.square
..isAntiAlias = true
..style = PaintingStyle.fill
..color = state.widget.tableColor;
tempDrawData.clear();
drawTable(canvas, size, paint1, paint2);
}
void drawTable(Canvas canvas, Size size, Paint paint1, Paint paint2) {
double totalColumnWidth = 0;
double columnWidth = 0;
List<double> totalReversalRowWidthList = [];
double totalReversalRowWidth = 0;
for (int i = _rowLength - 1; i >= 0; i--) {
totalReversalRowWidth += _rowWidthList[i][1];
totalReversalRowWidthList.add(totalReversalRowWidth);
}
for (int i = 0; i < _rowLength; i++) {
totalColumnWidth += columnWidth;
columnWidth = _rowWidthList[i][1];
for (int j = 0; j < _columnLength; j++) {
String str = _data![j][i] as String;
if (j < state.widget.lockList[0]) {
//上
if (i < state.widget.lockList[2]) {
//左上角
drawTableAdd(str, totalColumnWidth, _columnHeight * j, columnWidth,
_columnHeight, j, i,
level: 2);
} else if (i >= _rowLength - state.widget.lockList[3]) {
//右上角
drawTableAdd(
str,
state.widget.width -
totalReversalRowWidthList[_rowLength - i - 1],
_columnHeight * j,
columnWidth,
_columnHeight,
j,
i,
level: 2);
} else {
drawTableAdd(str, totalColumnWidth + _offsetDx, _columnHeight * j,
columnWidth, _columnHeight, j, i,
level: 1);
}
} else if (i < state.widget.lockList[2]) {
//左
if (j >= _columnLength - state.widget.lockList[1]) {
//左下角
drawTableAdd(
str,
totalColumnWidth,
state.widget.height - _columnHeight * (_columnLength - j),
columnWidth,
_columnHeight,
j,
i,
level: 2);
} else {
drawTableAdd(str, totalColumnWidth, _columnHeight * j + _offsetDy,
columnWidth, _columnHeight, j, i,
level: 1);
}
} else if (j >= _columnLength - state.widget.lockList[1]) {
//下
if (i >= _rowLength - state.widget.lockList[3]) {
// 右下角
drawTableAdd(
str,
state.widget.width -
totalReversalRowWidthList[_rowLength - i - 1],
state.widget.height - _columnHeight * (_columnLength - j),
columnWidth,
_columnHeight,
j,
i,
level: 2);
} else {
drawTableAdd(
str,
totalColumnWidth + _offsetDx,
state.widget.height - _columnHeight * (_columnLength - j),
columnWidth,
_columnHeight,
j,
i,
level: 1);
}
} else if (i >= _rowLength - state.widget.lockList[3]) {
//右
drawTableAdd(
str,
state.widget.width -
totalReversalRowWidthList[_rowLength - i - 1],
_columnHeight * j + _offsetDy,
columnWidth,
_columnHeight,
j,
i,
level: 1);
} else {
drawTableAdd(str, totalColumnWidth + _offsetDx,
_columnHeight * j + _offsetDy, columnWidth, _columnHeight, j, i);
}
}
}
drawTableReal(canvas, size, paint1, paint2);
}
/// 绘制对象缓存
final List<_TempDrawData> tempDrawData = <_TempDrawData>[];
/// 把需要绘制的数据先放入内存中
void drawTableAdd(String text, double left, double top, double width,
double height, int row, int column,
{int? level}) {
tempDrawData.add(
_TempDrawData(text, left, top, width, height, row, column, level ?? 0));
}
/// 遍历存好的数据进行绘制
void drawTableReal(Canvas canvas, Size size, Paint paint1, Paint paint2) {
//绘制层级排序
tempDrawData.sort((a, b) => a.level.compareTo(b.level));
//绘制
for (_TempDrawData data in tempDrawData) {
//构建文字
ui.ParagraphBuilder paragraphBuilder =
ui.ParagraphBuilder(ui.ParagraphStyle())
..pushStyle(ui.TextStyle(
color: Colors.redAccent, fontSize: state.widget.fontSize))
..addText(data.text);
//先初始化paint2的颜色
paint2.color = state.widget.tableColor;
//表格有指定颜色的颜色
if (state.widget.colorList.isNotEmpty) {
for (TableColor tableColor in state.widget.colorList) {
if (tableColor is RowColor && tableColor.index == data.row) {
paint2.color = tableColor.color;
} else if (tableColor is ColumnColor &&
tableColor.index == data.column) {
paint2.color = tableColor.color;
}
}
}
//画表格背景
canvas.drawRect(
Rect.fromLTWH(data.left, data.top, data.width, data.height), paint2);
//画表格边框
canvas.drawRect(
Rect.fromLTWH(data.left, data.top, data.width, data.height), paint1);
//画表格文本
canvas.drawParagraph(
paragraphBuilder.build()
..layout(ui.ParagraphConstraints(width: size.width)),
Offset(data.left, data.top));
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class _TempDrawData {
String text;
double left;
double top;
double width;
double height;
int row;
int column;
int level = 0;
_TempDrawData(this.text, this.left, this.top, this.width, this.height,
this.row, this.column, this.level);
}
abstract class TableColor {
final int index;
final Color color;
const TableColor(this.index, this.color);
}
class RowColor extends TableColor {
const RowColor(super.index, super.color);
}
class ColumnColor extends TableColor {
const ColumnColor(super.index, super.color);
}
代码拷贝下来可以直接使用,注释非常多,有看不明白的地方,可以讨论。
- 使用
构建测试数据:
List<List<String>> _getTableTestData() {
//模拟数据
List<List<String>> data = [];
Random random = Random();
for (int i = 0; i < 50; i++) {
List<String> strList = [];
for (int j = 0; j < 20; j++) {
int seed = random.nextInt(100);
strList.add(" $seed ");
}
data.add(strList);
}
return data;
}
使用:
KqTable<String>(
data: _getTableTestData(),
onTap: (String data) {
print(data);
},
),