Flutter用700行代码纯手工自定义绘制表格控件KqTable

我们项目中往往需要使用到自定义表格,系统提供的表格控件只支持简单的展示功能,并不能很好的满足我们项目的自定义,然而自定义最大自由度的还是自己绘制,所以我选则了自己从头开始自定义绘制一个表格,绘制中不仅需要考虑到事件的处理,还需要考虑到表格固定行列的处理,绘制时采用了数据预处理策略,先对需要绘制的数据进行层级排序,然后根据层级排序,控制绘制顺序。而且项目中还用到了局部绘制的概念,即只绘制出当前正在展示的表格,根据表格的滑动,动态绘制需要展示的内容。感兴趣的伙伴可以直接复制代码使用和修改。

  • 演示

  • 功能

1.支持动态数据绘制。数据格式[[row],[row],...]。

2.支持表格中文字的大小与颜色设置。

3.支持控件宽高设置。

4.支持设置表格的格子背景颜色与边框颜色。

5.支持固定上下左右行列,固定遵循格式,代表上下左右固定的行与列数:[int,int,int,int]

6.支持指定任意行列的颜色,设置格式:[TableColor,TableColor,...],TableColor有两个子函数。设置行颜色的RowColor与设置列颜色的RowColor。

7.支持单元格点击回调,回调会返回所点击的单元格的对应数据对象T。

8.支持行列拖拽宽度和高度。

9.支持点击表头选中行列,并高亮。

10.手把手自定义事件处理。

  • 代码
import 'dart:async';

import 'package:flutter/material.dart';
import 'dart:ui' as ui;

class KqTable<T extends ITableData> 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;

  /// 表格点击选中颜色
  final Color tableClickChosenColor;

  /// 表格长按选中颜色
  final Color tableLongPressChosenColor;

  /// 表格点击选中边框颜色
  final Color tableClickChosenBorderColor;

  /// 表格长按选中边框颜色
  final Color tableLongPressChosenBorderColor;

  /// 上下左右固定行数值[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 = const Color.fromARGB(255, 189, 189, 189),
      this.lockList = const [3, 0, 1, 0],
      this.colorList = const [
        RowColor(0, Color.fromARGB(255, 238, 238, 238)),
        RowColor(5, Color.fromARGB(255, 238, 238, 238)),
        RowColor(6, Color.fromARGB(255, 238, 238, 238)),
        ColumnColor(0, Color.fromARGB(255, 238, 238, 238)),
        ColumnColor(7, Color.fromARGB(255, 238, 238, 238))
      ],
      this.onTap,
      this.tableClickChosenColor =
          const Color.fromARGB(102, 130, 177, 255), //a=40% blue
      this.tableClickChosenBorderColor =
          const Color.fromARGB(230, 130, 177, 255), //90% blue
      this.tableLongPressChosenColor =
          const Color.fromARGB(102, 29, 233, 182), //a=40% green
      this.tableLongPressChosenBorderColor =
          const Color.fromARGB(230, 29, 233, 182)}); //a=90% green

  @override
  State<StatefulWidget> createState() => _KqTableState<T>();
}

class _KqTableState<T extends ITableData> extends State<KqTable<T>> {
  /// 长按判定等待时间100毫秒
  final int _waitTime = 100;

  /// x方向偏移量
  double _offsetDx = 0;

  /// y方向偏移量
  double _offsetDy = 0;

  /// x方向误差量
  double _diffOffsetDx = 0;

  /// y方向误差量
  double _diffOffsetDy = 0;

  /// 行数
  int _xLength = 0;

  /// 列数
  int _yLength = 0;

  /// 每列的行文本最大宽度列表[[原宽度,调整后宽度],[原宽度,调整后宽度],...]
  final List<List<double>> _xWidthList = [];

  /// 每行的文本高度,高度也可以变化,所以不能用一个值表达[[原高度,调整后高度],[原高度,调整后高度],...]
  final List<List<double>> _yHeightList = [];

  /// 按下时当前单元格的对象
  T? _opTableData;

  /// 当前手势是否滑动
  bool _opIsMove = false;

  /// 当前是否是长按
  bool _opIsLongPress = false;

  /// 绘制对象缓存
  final List<ITableData> _tempDrawData = <ITableData>[];

  /// 计时器
  Timer? timer;

  /// 点击选中行
  int _opClickChosenX = -1;

  /// 点击选中列
  int _opClickChosenY = -1;

  /// 长按选中行
  int _opLongPressChosenX = -1;

  /// 长按选中的行或者列的宽度或者高度值;
  double _opLongPressChosenWH = 0;

  /// 长按选中列
  int _opLongPressChosenY = -1;

  /// 点击是否同时选中行列
  bool _opIsClickChosenXY = false;

  /// 长按是否同时选中行列
  bool _opIsLongPressChosenXY = false;

  @override
  void initState() {
    super.initState();
    _initData();
  }

  @override
  void dispose() {
    //退出时关闭计时器防止内存泄露
    _stopLongPressTimer();
    super.dispose();
  }

  void _initData() {
    _xLength = widget.data[0].length;
    _yLength = widget.data.length;
    double columnHeight = 0;
    for (int i = 0; i < _xLength; i++) {
      double maxWidth = 0;
      for (int j = 0; j < _yLength; j++) {
        ITableData tableData = widget.data[j][i];
        TextPainter textPainter = TextPainter(
            text: TextSpan(
                text: tableData.text,
                style: TextStyle(
                    color: widget.textColor, fontSize: widget.fontSize)),
            maxLines: 1,
            textDirection: TextDirection.ltr)
          ..layout(minWidth: 0, maxWidth: double.infinity);
        if (maxWidth < textPainter.width) {
          maxWidth = textPainter.width;
        }
        columnHeight = textPainter.height;
      }
      _xWidthList.add([maxWidth, maxWidth]);
    }
    for (int j = 0; j < _yLength; j++) {
      _yHeightList.add([columnHeight, columnHeight]);
    }
  }

  void _startLongPressTimer(VoidCallback callback) {
    //计时器,每[_waitTime]毫秒执行一次
    var period = Duration(milliseconds: _waitTime);
    if (timer != null && timer!.isActive) {
      timer?.cancel();
    }
    timer = Timer(period, () {
      if (mounted) {
        _opIsLongPress = true;
        callback();
      }
    });
  }

  void _stopLongPressTimer() {
    timer?.cancel();
  }

  @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,
                  _xLength,
                  _yLength,
                  _xWidthList,
                  _yHeightList,
                  _tempDrawData,
                  _opClickChosenX,
                  _opClickChosenY,
                  _opLongPressChosenX,
                  _opLongPressChosenY,
                  _opIsClickChosenXY,
                  _opIsLongPressChosenXY),
            ),
            onPointerDown: (PointerDownEvent event) {
              _opIsMove = false;
              _opIsLongPress = false;
              _opIsLongPressChosenXY = false;
              _opIsClickChosenXY = false;
              _opLongPressChosenX = -1;
              _opLongPressChosenY = -1;
              _opClickChosenX = -1;
              _opClickChosenY = -1;
              //事件点击的中心位置
              Offset? eventOffset = event.localPosition;
              _diffOffsetDx = eventOffset.dx - _offsetDx;
              _diffOffsetDy = eventOffset.dy - _offsetDy;

              ///判定按下在哪个单元格,并获取单元格内容
              //点击的横向坐标
              int y = 0;
              //点击的纵向坐标
              int x = 0;
              //计算横向和纵向坐标
              ITableData? tempX;
              ITableData? tempY;
              for (ITableData tableData in _tempDrawData) {
                if (eventOffset.dx < (tableData.left! + tableData.width!) &&
                    eventOffset.dx > tableData.left!) {
                  if (tempX == null || tempX.level! < tableData.level!) {
                    tempX = tableData;
                  }
                }

                if (eventOffset.dy < (tableData.top! + tableData.height!) &&
                    eventOffset.dy > tableData.top!) {
                  if (tempY == null || tempY.level! < tableData.level!) {
                    tempY = tableData;
                  }
                }
              }
              if (tempX != null) {
                x = tempX.x!;
              }
              if (tempY != null) {
                y = tempY.y!;
              }
              // 单击单元格判定
              if (x == 0 && y == 0) {
                _opIsClickChosenXY = true;
              } else if (x == 0) {
                _opClickChosenY = y;
              } else if (y == 0) {
                _opClickChosenX = x;
              } else {
                _opIsClickChosenXY = false;
                _opClickChosenX = -1;
                _opClickChosenY = -1;
              }
              //获取坐标对应的值
              _opTableData = widget.data[y][x];

              /// 长按拖拽判定
              _startLongPressTimer(() {
                if (y == 0 && x != 0) {
                  // 判断宽度拖拽
                  _opLongPressChosenX = x;
                  _opLongPressChosenWH = _xWidthList[_opLongPressChosenX][1];
                } else if (x == 0 && y != 0) {
                  //判断高度拖拽
                  _opLongPressChosenY = y;
                  _opLongPressChosenWH = _yHeightList[_opLongPressChosenY][1];
                } else if (y == 0 && x == 0) {
                  //判断宽度和高度同时拖拽
                  _opIsLongPressChosenXY = true;
                }
                if (_opLongPressChosenX != -1 ||
                    _opLongPressChosenY != -1 ||
                    _opIsLongPressChosenXY) {
                  _opClickChosenX = -1;
                  _opClickChosenY = -1;
                  _opIsClickChosenXY = false;
                }
                setState(() {});
              });
            },
            onPointerMove: (PointerMoveEvent event) {
              _opIsMove = true;
              _stopLongPressTimer();
              //事件点击的中心位置
              Offset? eventOffset = event.localPosition;
              if (_opLongPressChosenX != -1) {
                ///表格宽度拖拽
                if (_xWidthList[_opLongPressChosenX][1] >
                        _xWidthList[_opLongPressChosenX][0] ||
                    ((eventOffset.dx - _diffOffsetDx) > 0 &&
                        _xWidthList[_opLongPressChosenX][1] ==
                            _xWidthList[_opLongPressChosenX][0])) {
                  _xWidthList[_opLongPressChosenX][1] =
                      _opLongPressChosenWH + eventOffset.dx - _diffOffsetDx;
                } else {
                  _xWidthList[_opLongPressChosenX][1] =
                      _xWidthList[_opLongPressChosenX][0];
                }
              } else if (_opLongPressChosenY != -1) {
                ///表格高度拖拽
                if (_yHeightList[_opLongPressChosenY][1] >
                    _yHeightList[_opLongPressChosenY][0] ||
                    ((eventOffset.dy - _diffOffsetDy) > 0 &&
                        _yHeightList[_opLongPressChosenY][1] ==
                            _yHeightList[_opLongPressChosenY][0])) {
                  _yHeightList[_opLongPressChosenY][1] =
                      _opLongPressChosenWH + eventOffset.dy - _diffOffsetDy;
                } else {
                  _yHeightList[_opLongPressChosenY][1] =
                      _yHeightList[_opLongPressChosenY][0];
                }
              } else if (_opIsLongPressChosenXY) {
                ///宽高同时拖拽
                if (eventOffset.dx >= _xWidthList[0][0]) {
                  _xWidthList[0][1] = eventOffset.dx;
                } else {
                  _xWidthList[0][1] = _xWidthList[0][0];
                }
                if (eventOffset.dy >= _yHeightList[0][0]) {
                  _yHeightList[0][1] = eventOffset.dy;
                } else {
                  _yHeightList[0][1] = _yHeightList[0][0];
                }
              } else {
                ///表格移动
                _offsetDx = eventOffset.dx - _diffOffsetDx;
                _offsetDy = eventOffset.dy - _diffOffsetDy;
              }

              /// 边界处理
              // 当有固定行时
              // 上边限定
              if (_offsetDy >= 0) {
                _offsetDy = 0;
              }
              // 左边限定
              if (_offsetDx >= 0) {
                _offsetDx = 0;
              }
              // 右边限定
              double rightOffset = 0;
              double tableWidth = _TableUtils.getTableRealWidth(_xWidthList);
              double tableHeight = _TableUtils.getTableRealHeight(_yHeightList);
              for (int i = 0; i < widget.lockList[3]; i++) {
                rightOffset += _xWidthList[_xWidthList.length - i - 1][1];
              }
              if (_offsetDx <= (widget.width + rightOffset) - tableWidth) {
                _offsetDx = (widget.width + rightOffset) - tableWidth;
              }
              // 下边限定
              List<double> reversalCellHeights =
                  _TableUtils.reversalCellHeights(_yLength, _yHeightList);
              if (_offsetDy <=
                  (widget.height +
                          reversalCellHeights[widget.lockList[1] == 0
                              ? 0
                              : widget.lockList[1] - 1]) -
                      tableHeight) {
                _offsetDy = (widget.height +
                        reversalCellHeights[widget.lockList[1] == 0
                            ? 0
                            : widget.lockList[1] - 1]) -
                    tableHeight;
              }
              //当表格宽度小于控件宽度,则不能水平移动
              if (tableWidth <= widget.width) {
                _offsetDx = 0;
              }
              //当表格高度小于控件高度,则不能上下移动
              if (tableHeight <= widget.height) {
                _offsetDy = 0;
              }

              setState(() {});
            },
            onPointerUp: (PointerUpEvent event) {
              if (_opIsLongPress) {
                //长按
                setState(() {
                  _opLongPressChosenX = -1;
                  _opLongPressChosenY = -1;
                  _opIsLongPressChosenXY = false;
                });
              } else if (!_opIsMove) {
                //单击
                setState(() {
                  _stopLongPressTimer();
                  widget.onTap?.call(_opTableData 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 _xLength;

  /// 列数
  final int _yLength;

  /// 每列的行文本最大宽度列表
  final List<List<double>> _xWidthList;

  /// 每行的文本高度列表
  final List<List<double>> _columnHeightList;

  /// 绘制对象缓存
  final List<ITableData> _tempDrawData;

  /// 点击选中行
  final int _opClickChosenX;

  /// 点击选中列
  final int _opClickChosenY;

  /// 长按选中行
  final int _opLongPressChosenX;

  /// 长按选中列
  final int _opLongPressChosenY;

  /// 点击是否同时选中行列
  final bool _opIsClickChosenXY;

  /// 长按是否同时选中行列
  final bool _opIsLongPressChosenXY;

  _TablePainter(
      this.state,
      this._offsetDx,
      this._offsetDy,
      this._data,
      this._xLength,
      this._yLength,
      this._xWidthList,
      this._columnHeightList,
      this._tempDrawData,
      this._opClickChosenX,
      this._opClickChosenY,
      this._opLongPressChosenX,
      this._opLongPressChosenY,
      this._opIsClickChosenXY,
      this._opIsLongPressChosenXY);

  @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) {
    List<double> reversalRowWidths =
        _TableUtils.reversalCellWidths(_xLength, _xWidthList);
    List<double> reversalColumnHeights =
        _TableUtils.reversalCellHeights(_yLength, _columnHeightList);
    double totalCellWidth = 0;
    double cellWidth = 0;
    for (int i = 0; i < _xLength; i++) {
      totalCellWidth += cellWidth;
      cellWidth = _xWidthList[i][1];
      double totalCellHeight = 0;
      double cellHeight = 0;
      if (totalCellWidth + _offsetDx <= state.widget.width) {
        for (int j = 0; j < _yLength; j++) {
          String str = (_data![j][i] as ITableData).text;
          totalCellHeight += cellHeight;
          cellHeight = _columnHeightList[j][1];
          if (totalCellHeight + _offsetDy <= state.widget.height) {
            if (j < state.widget.lockList[0]) {
              //上
              if (i < state.widget.lockList[2]) {
                //左上角
                drawTableAdd(str, totalCellWidth, totalCellHeight, cellWidth,
                    cellHeight, j, i,
                    level: 2);
              } else if (i >= _xLength - state.widget.lockList[3]) {
                //右上角
                drawTableAdd(
                    str,
                    state.widget.width - reversalRowWidths[_xLength - i - 1],
                    totalCellHeight,
                    cellWidth,
                    cellHeight,
                    j,
                    i,
                    level: 2);
              } else {
                drawTableAdd(str, totalCellWidth + _offsetDx, totalCellHeight,
                    cellWidth, cellHeight, j, i,
                    level: 1);
              }
            } else if (i < state.widget.lockList[2]) {
              //左
              if (j >= _yLength - state.widget.lockList[1]) {
                //左下角
                drawTableAdd(
                    str,
                    totalCellWidth,
                    state.widget.height -
                        reversalColumnHeights[_yLength - j - 1],
                    cellWidth,
                    cellHeight,
                    j,
                    i,
                    level: 2);
              } else {
                drawTableAdd(str, totalCellWidth, totalCellHeight + _offsetDy,
                    cellWidth, cellHeight, j, i,
                    level: 1);
              }
            } else if (j >= _yLength - state.widget.lockList[1]) {
              //下
              if (i >= _xLength - state.widget.lockList[3]) {
                // 右下角
                drawTableAdd(
                    str,
                    state.widget.width - reversalRowWidths[_xLength - i - 1],
                    state.widget.height -
                        reversalColumnHeights[_yLength - j - 1],
                    cellWidth,
                    cellHeight,
                    j,
                    i,
                    level: 2);
              } else {
                drawTableAdd(
                    str,
                    totalCellWidth + _offsetDx,
                    state.widget.height -
                        reversalColumnHeights[_yLength - j - 1],
                    cellWidth,
                    cellHeight,
                    j,
                    i,
                    level: 1);
              }
            } else if (i >= _xLength - state.widget.lockList[3]) {
              //右
              drawTableAdd(
                  str,
                  state.widget.width - reversalRowWidths[_xLength - i - 1],
                  totalCellHeight + _offsetDy,
                  cellWidth,
                  cellHeight,
                  j,
                  i,
                  level: 1);
            } else {
              drawTableAdd(str, totalCellWidth + _offsetDx,
                  totalCellHeight + _offsetDy, cellWidth, cellHeight, j, i);
            }
          }
        }
      }
    }

    drawTableReal(canvas, size, paint1, paint2);
  }

  /// 把需要绘制的数据先放入内存中
  void drawTableAdd(String text, double left, double top, double width,
      double height, int y, int x,
      {int? level}) {
    if (top <= state.widget.height && left <= state.widget.width) {
      _tempDrawData.add(ITableData(text,
          left: left,
          top: top,
          width: width,
          height: height,
          y: y,
          x: x,
          level: level ?? 0));
    }
  }

  /// 遍历存好的数据进行绘制
  void drawTableReal(Canvas canvas, Size size, Paint paint1, Paint paint2) {
    //绘制层级排序
    _tempDrawData.sort((a, b) => a.level!.compareTo(b.level!));
    //绘制
    for (ITableData data in _tempDrawData) {
      if (data.top! <= state.widget.height &&
          data.left! <= state.widget.width) {
        //构建文字
        ui.ParagraphBuilder paragraphBuilder =
            ui.ParagraphBuilder(ui.ParagraphStyle())
              ..pushStyle(ui.TextStyle(
                  color: state.widget.textColor, 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.y) {
              paint2.color = tableColor.color;
            } else if (tableColor is ColumnColor &&
                tableColor.index == data.x) {
              paint2.color = tableColor.color;
            }
          }
        }

        ///画表格背景
        canvas.drawRect(
            Rect.fromLTWH(data.left!, data.top!, data.width!, data.height!),
            paint2);

        //画笔颜色调整,主要针点击背景覆盖层和边框绘制
        if ((_opLongPressChosenX != -1 && data.x == _opLongPressChosenX) ||
            (_opLongPressChosenY != -1 && data.y == _opLongPressChosenY) ||
            (_opIsLongPressChosenXY && (data.x == 0 || data.y == 0))) {
          paint2.color = state.widget.tableLongPressChosenColor;
          paint1.color = state.widget.tableLongPressChosenBorderColor;

          ///画表格覆盖背景
          canvas.drawRect(
              Rect.fromLTWH(data.left!, data.top!, data.width!, data.height!),
              paint2);
        } else if ((_opClickChosenX != -1 && data.x == _opClickChosenX) ||
            (_opClickChosenY != -1 && data.y == _opClickChosenY) ||
            (_opIsClickChosenXY && (data.x == 0 || data.y == 0))) {
          paint2.color = state.widget.tableClickChosenColor;
          paint1.color = state.widget.tableClickChosenBorderColor;

          ///画表格覆盖背景
          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;
  }
}

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);
}

class ITableData {
  final String text;
  double? left;
  double? top;
  double? width;
  double? height;
  int? y;
  int? x;
  int? level = 0;

  ITableData(this.text,
      {this.left,
      this.top,
      this.width,
      this.height,
      this.y,
      this.x,
      this.level});

  @override
  String toString() {
    return "text=$text,left=$left,top=$top,width=$width,height=$height,row=$y,column=$x,level=$level";
  }
}

class _TableUtils {
  /// 单元格宽度反向长度列表
  static List<double> reversalCellWidths(
      int xLength, List<List<double>> xWidthList) {
    List<double> totalReversalCellWidthList = [];
    double totalReversalCellWidth = 0;
    for (int i = xLength - 1; i >= 0; i--) {
      totalReversalCellWidth += xWidthList[i][1];
      totalReversalCellWidthList.add(totalReversalCellWidth);
    }
    return totalReversalCellWidthList;
  }

  /// 单元格高度反向高度列表
  static List<double> reversalCellHeights(
      int yLength, List<List<double>> yHeightList) {
    List<double> totalReversalCellHeightList = [];
    double totalReversalCellHeight = 0;
    for (int i = yLength - 1; i >= 0; i--) {
      totalReversalCellHeight += yHeightList[i][1];
      totalReversalCellHeightList.add(totalReversalCellHeight);
    }
    return totalReversalCellHeightList;
  }

  /// 获取表格宽高
  static double getTableRealWidth(List<List<double>> xWidthList) {
    double totalWidth = 0;
    for (int i = 0; i < xWidthList.length; i++) {
      totalWidth += xWidthList[i][1];
    }
    return totalWidth;
  }

  /// 获取表格宽高
  static double getTableRealHeight(List<List<double>> yHeightList) {
    double totalHeight = 0;
    for (int i = 0; i < yHeightList.length; i++) {
      totalHeight += yHeightList[i][1];
    }
    return totalHeight;
  }
}
  • 使用

构建测试数据:

class TestTableData extends ITableData {
  TestTableData(super.text);
}


List<List<TestTableData>> _getTableTestData2() {
    //模拟数据
    List<List<TestTableData>> data = [];
    Random random = Random();

    for (int i = 0; i < 20; i++) {
      List<TestTableData> dataList = [];
      for (int j = 0; j < 10; j++) {
        int seed = random.nextInt(100);
        dataList.add(TestTableData(" $seed "));
      }
      data.add(dataList);
    }
    return data;
}

使用:

KqTable<TestTableData>(
    data: _getTableTestData2(),
    onTap: (TestTableData data) {
        KqToast.showNormal(data.text);
    },
)

代码注释全面,有需要的朋友可以直接撸,如有问题,欢迎指正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/685.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

linux目录——文件管理

个人简介&#xff1a;云计算网络运维专业人员&#xff0c;了解运维知识&#xff0c;掌握TCP/IP协议&#xff0c;每天分享网络运维知识与技能。座右铭&#xff1a;海不辞水&#xff0c;故能成其大&#xff1b;山不辞石&#xff0c;故能成其高。个人主页&#xff1a;小李会科技的…

【C#】组件化开发,调用dll组件方法

系列文章 C#项目–业务单据号生成器&#xff08;定义规则、自动编号、流水号&#xff09; 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/129129787 C#项目–开始日期结束日期范围计算&#xff08;上周、本周、明年、前年等&#xff09; 本文链接&…

UE笔记-AI Move To无法正常结束/打断 1

启用Stop on Overlap 会导致AI与目标距离受到碰撞影响&#xff0c;实际效果需按要求处理 当Lock AILogic为True时&#xff0c;Move To的Task无法被黑板装饰器打断 当Use Continuos Goal Tracking为True时&#xff0c;Move To的节点不会根据Acceptance Radius设定而结束&#x…

这两天最好的ChatGPT应用;使用Notion AI提升效率的经验(13);AI编程与程序员的生存 | ShowMeAI日报

&#x1f440;日报合辑 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f916; 硅谷银行风波中&#xff0c;OpenAI 创始人大方帮助硅谷初创公司&#xff1a;钱先拿着用&#xff0c;有了再还 OpenAI 创始人 Sam Altman 的弟弟…

数据库基础语法

sql&#xff08;Structured Query Language 结构化查询语言&#xff09; SQL语法 use DataTableName; 命令用于选择数据库。set names utf8; 命令用于设置使用的字符集。SELECT * FROM Websites; 读取数据表的信息。上面的表包含五条记录&#xff08;每一条对应一个网站信息&…

三天吃透计算机网络面试八股文

本文已经收录到Github仓库&#xff0c;该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点&#xff0c;欢迎star~ Github地址&#xff1a;https://github.com/…

stm32外设-GPIO

0. 写在最前 本栏目笔记都是基于stm32F10x 1. GPIO基本介绍 GPIO—general purpose intput output 是通用输入输出端口的简称&#xff0c;简单来说就是软件可控制的引脚&#xff0c; STM32芯片的GPIO引脚与外部设备连接起来&#xff0c;从而实现与外部通讯、控制以及数据采集的…

802.1x认证和MAC认证讲解

目录 802.1x基础 EAP&#xff08;Extensible Authentication Protocol&#xff09;可扩展认证协议 EAPoL&#xff08;EAP over LAN&#xff09;局域网可扩展认证协议 802.1x体系架构 受控端口的受控方式 802.1x认证 802.1x认证触发方式 客户端退出认证 802.1x认证方式…

【云原生】Linux进程控制(创建、终止、等待)

✨个人主页&#xff1a; Yohifo &#x1f389;所属专栏&#xff1a; Linux学习之旅 &#x1f38a;每篇一句&#xff1a; 图片来源 &#x1f383;操作环境&#xff1a; CentOS 7.6 阿里云远程服务器 Good judgment comes from experience, and a lot of that comes from bad jud…

一年经验年初被裁面试1月有余无果,还遭前阿里面试官狂问八股,人麻了

最近接到一粉丝投稿&#xff1a;年初被裁员&#xff0c;在家躺平了6个月&#xff0c;然后想着学习下再去面试&#xff0c;现在面试了1个月有余&#xff0c;无果&#xff0c;天天打游戏到半夜&#xff0c;根本无法静下心来学习。下面是他这些天面试经常会被问到的一些问题&#…

【笔记】效率之门——Python中的函数式编程技巧

文章目录Python函数式编程1. 数据2. 推导式3. 函数式编程3.1. Lambda函数3.2. python内置函数3.3. 高阶函数4. 函数式编程的应用Python函数式编程 我的AI Studio项目&#xff1a;【笔记】LearnDL第三课&#xff1a;Python高级编程——抽象与封装 - 飞桨AI Studio (baidu.com) p…

【CSS】盒子模型内边距 ② ( 内边距复合写法 | 代码示例 )

文章目录一、内边距复合写法1、语法2、代码示例 - 设置 1 个值3、代码示例 - 设置 2 个值4、代码示例 - 设置 3 个值5、代码示例 - 设置 4 个值一、内边距复合写法 1、语法 盒子模型内边距 可以通过 padding-left 左内边距padding-right 右内边距padding-top 上内边距padding-…

【数据结构】第二站:顺序表

目录 一、线性表 二、顺序表 1.顺序表的概念以及结构 2.顺序表的接口实现 3.顺序表完整代码 三、顺序表的经典题目 1.移除元素 2.删除有序数组中的重复项 3.合并两个有序数组 一、线性表 在了解顺序表前&#xff0c;我们得先了解线性表的概念 线性表&#xff08;linear…

网站动态背景 | vanta.js的使用

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录前言一、Vanta.js是什么&#xff1f;二、使用步骤1.引入库在项目中安装 three.js 依赖在项目中安装 Vanta JS 依赖2.代码部分&#xff0c;在具体项目中调用&#xff…

分享几个常用的运维 shell 脚本

今天咸鱼给大家分享几个不错的 Linux 运维脚本&#xff0c;这些脚本中大量使用了 Linux 的文本三剑客&#xff1a; awkgrepsed 建议大家这三个工具都要了解并最好能够较为熟练的使用 根据 PID 显示进程所有信息 根据用户输入的PID&#xff0c;过滤出该PID所有的信息 #! /b…

春分策划×运维老王主讲:CMDB数据运营精准化公开课启动报名啦!

『CMDB数据运营精准化』 公开直播课 要来了&#xff01; &#x1f446;扫描海报二维码&#xff0c;预约直播 CMDB似乎是运维中永恒的老话题。 提到CMDB很多人都是又爱又恨&#xff0c;爱的是它给我们提供了一个美好的未来&#xff0c;有了CMDB我们可以解决诸多运维中的难题。…

常见的Web安全漏洞:SYN攻击/CSRF/XSS

一、SYN攻击&#xff08;属于DOS攻击&#xff09; 什么情况下被动方出现SYN_RCVD状态?(flood攻击服务) 客户伪造 ip 端口&#xff0c; 向服务端发送SYN请求。完成2次握手&#xff0c;第三次服务端 等待客户端ACK确认&#xff0c;但由于客户不存在服务端一直未收到确认&#…

Rockchip RV1126 模型部署(完整部署流程)

文章目录1、芯片简介2、部署流程简述3、开发环境配置&#xff08;RKNN-Toolkit&#xff09;3.1、软件安装测试3.2、示例代码解析4、开发环境配置&#xff08;RKNN-NPU&#xff09;4.1、源码结构4.2、 编译源码4.3、源码解析4.4、芯片端运行5、量化算法解析1、芯片简介 环境概述…

动态矢量瓦片缓存库方案

目录 前言 二、实现步骤 1.将数据写入postgis数据库 2.将矢量瓦片数据写入缓存库 3.瓦片接口实现 4.瓦片局部更新接口实现 总结 前言 矢量瓦片作为webgis目前最优秀的数据格式&#xff0c;其主要特点就是解决了大批量数据在前端渲染时出现加载缓慢、卡顿的问题&#xff0…

Spark SQL支持DataFrame操作的数据源

DataFrame提供统一接口加载和保存数据源中的数据&#xff0c;包括&#xff1a;结构化数据、Parquet文件、JSON文件、Hive表&#xff0c;以及通过JDBC连接外部数据源。一个DataFrame可以作为普通的RDD操作&#xff0c;也可以通过&#xff08;registerTempTable&#xff09;注册成…
最新文章