如何在Flutter中实现固定垂直和水平标题的表格?例如,水平标题只能水平滚动,垂直标题只能垂直滚动。两个标题应该始终可见。如何设置布局?
我已经尝试使用带有两个嵌套列的行来设置整体 2x2 布局: (0, 0) 空; (0, 1) 垂直标题; (1,0) 水平标题和(1, 1) 数据。为了可视化实际数据,我使用 GridViews 来显示两个标题和数据。而且,我想使用滚动控制器来实现滚动行为。
我想到的另一个解决方案是嵌套 GridView,而不是 Row 和两列。
此代码显示第一列:
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。 ═════════════════════════════════
可能是某些宽度/高度属性设置不正确?您将如何实现这种表格布局?感谢您的帮助!
实际上有现有的 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);
}
实际产量:
您可以检查其他现有插件。
另外,这里有一些与您相关的已回答的问题:
其他参考,可以查看博客《Flutter:创建固定头和列的双向滚动表》
您可以简单地创建一个组件,其中包含一个列,其中放置 2 个表。该表将共享相同的列宽度。第一个表将用作标题,第二个表将用作行内容。将第二个表放入 Expanded 内的 SingleChildScrollView 中。
您可以在 youtube 上观看我的视频:https://youtu.be/yXBwiOThvIA