Flutter纯手工绘制定制表格控件Table

该项目为flutter表格的初级版,后续更完善的版本已发布文章:Flutter用700行代码纯手工自定义绘制表格控件KqTableicon-default.png?t=N176https://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);
    },
),
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许天成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值