【问题标题】:How to implement a table with fixed headers in Flutter?如何在 Flutter 中实现带有固定表头的表格?
【发布时间】:2020-01-26 05:04:35
【问题描述】:

如何在 Flutter 中实现一个固定垂直和水平标题的表格?例如,水平标题只能水平滚动,而垂直标题只能垂直滚动。两个标题都应该始终可见。如何设置布局?

Example

我已经尝试使用带有两个嵌套列的行来设置整体 2x2 布局:(0, 0) 空; (0, 1) 垂直标题; (1,0) 水平标题和 (1, 1) 数据。为了可视化实际数据,我对两个标题和数据使用了 GridViews。此外,我想使用滚动控制器来实现滚动行为。

    1. 列:(0) 空,(1) GridView
    2. 列:(0) GridView (1) GridView

我想到的另一个解决方案是嵌套 GridViews 而不是 Row 和两个 Columns。

此代码显示第一列:

Widget build(BuildContext context) {
    return Container(
      width: double.maxFinite,
      child: Row(
        children: <Widget>[
            Column(
              children: <Widget>[
                Text("empty"), // (0,0)
                Container( // (0, 1)
                  child: Flexible(
                    child: GridView.count(
                      controller: _vScrollController1,
                      crossAxisCount: 1,
                      childAspectRatio: 3.0,
                      children: List.generate(
                        widget.data.length,
                        (index) => Text("my cell")
                      ),
                    ),
                  ),
                ),
              ],
            ),
        ],
      ),
    );

但是,它会产生以下错误消息:

════════ 渲染库捕获的异常
在 null 上调用了方法“>”。
接收方:空。 尝试调用:>(1e-10)。 导致错误的小部件的用户创建的祖先是 Container。 ═════════════════════════════════

可能是某些宽度/高度属性设置不正确?你将如何实现这个表格布局?感谢您的帮助!

【问题讨论】:

  • 这里有一个额外的句号 --> Container(. // (0, 1)
  • 谢谢,我在示例中修复了它。我的问题是关于如何获得这种类型的布局更笼统。
  • 这里的主要问题是您正在使用一个小部件,该小部件可以增长到一个小部件内部的所有可用大小,该小部件没有固定大小(它是无限大小),所以外部小部件有一个无限大小和内部小部件需要在绘制之前知道它可以采用的大小。解决方案是限制外部小部件的大小。
  • 顺便说一句,你可以试试Table 小部件吗?见youtube.com/watch?v=_lbE0wsVZSwDataTable

标签: flutter flutter-dependencies


【解决方案1】:

实际上已有 Flutter 插件可以解决这个问题。考虑使用一个。

这是取自horizontal_data_table的示例:

import 'package:flutter/material.dart';
import 'package:horizontal_data_table/horizontal_data_table.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  HDTRefreshController _hdtRefreshController = HDTRefreshController();

  static const int sortName = 0;
  static const int sortStatus = 1;
  bool isAscending = true;
  int sortType = sortName;

  @override
  void initState() {
    user.initData(100);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: _getBodyWidget(),
    );
  }

  Widget _getBodyWidget() {
    return Container(
      child: HorizontalDataTable(
        leftHandSideColumnWidth: 100,
        rightHandSideColumnWidth: 600,
        isFixedHeader: true,
        headerWidgets: _getTitleWidget(),
        leftSideItemBuilder: _generateFirstColumnRow,
        rightSideItemBuilder: _generateRightHandSideColumnRow,
        itemCount: user.userInfo.length,
        rowSeparatorWidget: const Divider(
          color: Colors.black54,
          height: 1.0,
          thickness: 0.0,
        ),
        leftHandSideColBackgroundColor: Color(0xFFFFFFFF),
        rightHandSideColBackgroundColor: Color(0xFFFFFFFF),
        verticalScrollbarStyle: const ScrollbarStyle(
          thumbColor: Colors.yellow,
          isAlwaysShown: true,
          thickness: 4.0,
          radius: Radius.circular(5.0),
        ),
        horizontalScrollbarStyle: const ScrollbarStyle(
          thumbColor: Colors.red,
          isAlwaysShown: true,
          thickness: 4.0,
          radius: Radius.circular(5.0),
        ),
        enablePullToRefresh: true,
        refreshIndicator: const WaterDropHeader(),
        refreshIndicatorHeight: 60,
        onRefresh: () async {
          //Do sth
          await Future.delayed(const Duration(milliseconds: 500));
          _hdtRefreshController.refreshCompleted();
        },
        htdRefreshController: _hdtRefreshController,
      ),
      height: MediaQuery.of(context).size.height,
    );
  }

  List<Widget> _getTitleWidget() {
    return [
      TextButton(
        style: TextButton.styleFrom(
          padding: EdgeInsets.zero,
        ),
        child: _getTitleItemWidget(
            'Name' + (sortType == sortName ? (isAscending ? '↓' : '↑') : ''),
            100),
        onPressed: () {
          sortType = sortName;
          isAscending = !isAscending;
          user.sortName(isAscending);
          setState(() {});
        },
      ),
      TextButton(
        style: TextButton.styleFrom(
          padding: EdgeInsets.zero,
        ),
        child: _getTitleItemWidget(
            'Status' +
                (sortType == sortStatus ? (isAscending ? '↓' : '↑') : ''),
            100),
        onPressed: () {
          sortType = sortStatus;
          isAscending = !isAscending;
          user.sortStatus(isAscending);
          setState(() {});
        },
      ),
      _getTitleItemWidget('Phone', 200),
      _getTitleItemWidget('Register', 100),
      _getTitleItemWidget('Termination', 200),
    ];
  }

  Widget _getTitleItemWidget(String label, double width) {
    return Container(
      child: Text(label, style: TextStyle(fontWeight: FontWeight.bold)),
      width: width,
      height: 56,
      padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
      alignment: Alignment.centerLeft,
    );
  }

  Widget _generateFirstColumnRow(BuildContext context, int index) {
    return Container(
      child: Text(user.userInfo[index].name),
      width: 100,
      height: 52,
      padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
      alignment: Alignment.centerLeft,
    );
  }

  Widget _generateRightHandSideColumnRow(BuildContext context, int index) {
    return Row(
      children: <Widget>[
        Container(
          child: Row(
            children: <Widget>[
              Icon(
                  user.userInfo[index].status
                      ? Icons.notifications_off
                      : Icons.notifications_active,
                  color:
                      user.userInfo[index].status ? Colors.red : Colors.green),
              Text(user.userInfo[index].status ? 'Disabled' : 'Active')
            ],
          ),
          width: 100,
          height: 52,
          padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
          alignment: Alignment.centerLeft,
        ),
        Container(
          child: Text(user.userInfo[index].phone),
          width: 200,
          height: 52,
          padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
          alignment: Alignment.centerLeft,
        ),
        Container(
          child: Text(user.userInfo[index].registerDate),
          width: 100,
          height: 52,
          padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
          alignment: Alignment.centerLeft,
        ),
        Container(
          child: Text(user.userInfo[index].terminationDate),
          width: 200,
          height: 52,
          padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
          alignment: Alignment.centerLeft,
        ),
      ],
    );
  }
}

User user = User();

class User {
  List<UserInfo> userInfo = [];

  void initData(int size) {
    for (int i = 0; i < size; i++) {
      userInfo.add(UserInfo(
          "User_$i", i % 3 == 0, '+001 9999 9999', '2019-01-01', 'N/A'));
    }
  }

  ///
  /// Single sort, sort Name's id
  void sortName(bool isAscending) {
    userInfo.sort((a, b) {
      int aId = int.tryParse(a.name.replaceFirst('User_', '')) ?? 0;
      int bId = int.tryParse(b.name.replaceFirst('User_', '')) ?? 0;
      return (aId - bId) * (isAscending ? 1 : -1);
    });
  }

  ///
  /// sort with Status and Name as the 2nd Sort
  void sortStatus(bool isAscending) {
    userInfo.sort((a, b) {
      if (a.status == b.status) {
        int aId = int.tryParse(a.name.replaceFirst('User_', '')) ?? 0;
        int bId = int.tryParse(b.name.replaceFirst('User_', '')) ?? 0;
        return (aId - bId);
      } else if (a.status) {
        return isAscending ? 1 : -1;
      } else {
        return isAscending ? -1 : 1;
      }
    });
  }
}

class UserInfo {
  String name;
  bool status;
  String phone;
  String registerDate;
  String terminationDate;

  UserInfo(this.name, this.status, this.phone, this.registerDate,
      this.terminationDate);
}

实际输出:

您可以检查其他现有插件。

另外,这里有一些与您相关的已回答的 SO 问题:

其他参考可以查看博客“Flutter: Creating a two direction scrolling table with fixed head and column

【讨论】:

    猜你喜欢
    • 2020-08-20
    • 1970-01-01
    • 1970-01-01
    • 2014-03-21
    • 2020-05-21
    • 2013-07-08
    • 2015-01-07
    • 1970-01-01
    • 2019-11-18
    相关资源
    最近更新 更多