flutter-固定标题的可滚动表格

如图,想要实现这种表格样式,如何实现呢?直接上代码喽...


2441629273978_.pic.jpg

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

你可能感兴趣的:(flutter-固定标题的可滚动表格)