在项目开发中,报表是一个很常见的功能,有利于使用者一眼能看出数据的趋势与规律,非常适合数量大,且种类繁多的数据查看与对比。虽然Flutter提供了 Table,DataTable等相关的组件,但是在实际项目开发中,功能、扩展性、实用性、灵活性等十分有限,可以说几乎不可能不经调整修改能直接用于生产项目,笔者这次将详细讲解如何运用Flutter技术开发报表。
效果预览 需求实现实现需求 | 是否实现 |
---|---|
Android、iOS跨平台 | ✅ |
首列固定 | ✅ |
标题行固定 | ✅ |
首列跟随内容列上下滑动联动 | ✅ |
标题行跟随内容行左右滑动联动 | ✅ |
所需环境 | 功能描述 |
---|---|
Flutter 1.22.6.stable | 跨平台的UI框架 |
flutter_screenutil: 4.0.4+1 | Flutter屏幕适配插件 |
在 pubspec.yaml 文件中添加依赖
技术分析flutter_screenutil: 4.0.4+1
将整个报表分为4个部分,左上的固定列标题,左下的固定列、右上的标题行、右下的内容。由此看出,Table、Datatable 是不适用于这种功能的开发,所以笔者决定用 ListView来实现这个需求。固定列、标题行、内容3个部分采用ListView,左边的固定列是垂直滑动的ListView,右上面的标题行是 水平滑动的ListVie,右下面的是 既可以垂直滑动,又可以水平滑动的ListView,而左上的固定列标题使用普通的组件。这样就可以实现仿Excel的报表。功能分解可以参考如下图所示
技术实现 初步定义组件组件所需要的无非是 数据源、标题行,另外增加了一些样式参数,代码如下
class DataGrid extends StatefulWidget {
final List
单元格
单元格的渲染分为两种,被合并的单元格与普通的单元格。普通单元格需要有四面边框,而被合并的单元格则需要去除相对应的边框。
定义渲染边框的方法,控制单元格的边框渲染,代码如下所示
Border _buildBorderSide({bool hideLeft = false, bool hideRight = false, bool hideTop = false, bool hideBottom = false}) {
final double borderWidth = 0.33;
return Border(
bottom: hideBottom ?
BorderSide.none
:
BorderSide(width: borderWidth, color: widget.borderColor ?? widget.LIGHT_GREY),
top: hideTop ?
BorderSide.none
:
BorderSide(width: borderWidth, color: widget.borderColor ?? widget.LIGHT_GREY),
right: hideRight ?
BorderSide.none
:
BorderSide(width: borderWidth, color: widget.borderColor ?? widget.LIGHT_GREY),
left: hideLeft ?
BorderSide.none
:
BorderSide(width: borderWidth, color: widget.borderColor ?? widget.LIGHT_GREY)
);
}
构件单元格的方法如下所示
Widget _buildCell(String title, {
bool hideLeft = false, bool hideRight = false, bool hideTop = false,
bool hideBottom = false, bool hideTitle = false, Color bgColor}) {
return IntrinsicHeight(
child: Container(
alignment: widget.cellAlignment ?? Alignment.center,
padding: widget.cellPadding ?? EdgeInsets.fromLTRB(0, 15.0.h, 0, 15.h),
decoration: BoxDecoration(
border: _buildBorderSide(
hideLeft: hideLeft,
hideRight: hideRight,
hideTop: hideTop,
hideBottom: hideBottom
),
color: bgColor
),
child: Opacity(
opacity: hideTitle ? 0 : 1,
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14, color: widget.borderColor ?? ColorHelper.TEXT_BLACK),
))
)
);
}
温馨提示:上面代码的 IntrinsicHeight 组件可能用的很少,这是一个智能根据子组件的高度自动调整的组件,类似于 Android 的 wrap_content
构件空单元格
Widget _buildEmptyCell() {
return IntrinsicHeight(
child: Container(
alignment: widget.cellAlignment ?? Alignment.center,
padding: widget.cellPadding ?? EdgeInsets.fromLTRB(0, 20.0.h, 0, 20.h),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 0.33, color: widget.borderColor ?? ColorHelper.LIGHT_GREY),
top: BorderSide(width: 0.33, color: widget.borderColor ?? ColorHelper.LIGHT_GREY),
right: BorderSide(width: 0.33, color: widget.borderColor ?? ColorHelper.LIGHT_GREY),
left: BorderSide(width: 0.33, color: widget.borderColor ?? ColorHelper.LIGHT_GREY),
)
),
child: Text(
'',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14, color: widget.borderColor ?? ColorHelper.TEXT_BLACK),
)
)
);
}
技术难点
如何让垂直滑动内容的时候联动标题列移动如何让水平滑动内容的时候联动标题行移动如何让内容既可以水平滑动又可以垂直滑动
解决方案
针对问题 1, 2 ,每个可滚动的组件都可以用一个 ScrollController 来控制、监听、记录滚动的位置,所以我们可以利用ScrollController来实现联动 。定义4个 ScrollController 对象,一个负责固定列,一个负责标题行、一个负责内容的横向滚动,一个负责内容的纵向滚动,代码如下
//定义可控制滚动组件
ScrollController firstColumnController = ScrollController();
ScrollController secondColumnController = ScrollController();
ScrollController firstRowController = ScrollController();
ScrollController secondedRowController = ScrollController();
// 固定列的宽度
final double columnWidth = 780.0.w;
在 initState方法里面,绑定各个ScrollController对象的联动关系
@override
void initState() {
super.initState();
//监听固定列滚动
firstColumnController.addListener(() {
if (firstColumnController.offset != secondColumnController.offset) {
secondColumnController.jumpTo(firstColumnController.offset);
}
});
//监听第内容行的纵向滚动
secondColumnController.addListener(() {
if (firstColumnController.offset != secondColumnController.offset) {
firstColumnController.jumpTo(secondColumnController.offset);
}
});
//监听标题行的滚动
firstRowController.addListener(() {
if (firstRowController.offset != secondedRowController.offset) {
secondedRowController.jumpTo(firstRowController.offset);
}
});
//监听第内容行的横向滚动
secondedRowController.addListener(() {
if (firstRowController.offset != secondedRowController.offset) {
firstRowController.jumpTo(secondedRowController.offset);
}
});
}
固定列与固定单元格 代码如下
Container(
width: 300.w,
height: 1900.h,
child: Column(
children: [
Table(
children: [
TableRow(
children: [
_buildCell(
'${widget.fixedTitle ?? ''}',
hideBottom: true,
hideTop: true,
hideLeft: true,
bgColor: ColorHelper.LIGHT_GREY
),
]
),
],
),
Expanded(
child: ListView(
controller: firstColumnController,
children: [
Table(children: _buildTableColumnOne()),
],
),
),
],
),
),
实现既可以纵向滚动又可以横向滚动的内容
针对问题3,可以采用 ListView内部嵌套 SingleChildScrollView组件来实现内容的既可以横向滚动又可以纵向滚动,SingleChildScrollView负责横向滚动,ListView负责纵向滚动,并且SingleChildScrollView必须有一个确定的宽度。具体代码可参考如下所示
ListView(
controller: thirdColumnController,
children: [
SingleChildScrollView(
controller: secondedRowController,
scrollDirection: Axis.horizontal,
child: IntrinsicWidth(
child: Container(
padding: EdgeInsets.only(bottom: 10.h),
// 避免行数未填满时,下边框消失
child: ...
width: 1000.w
)
)
)
],
)
完整代码如下
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_study/common/util/color_helper.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
/// 数据表格
class DataGrid extends StatefulWidget {
final List
使用该组件
代码如下
import 'package:flutter/material.dart';
import 'package:flutter_study/common/ui/datagrid.dart';
class TestScrollView extends StatefulWidget {
const TestScrollView({Key key}) : super(key: key);
@override
_TestScrollViewState createState() => _TestScrollViewState();
}
class _TestScrollViewState extends State {
List datas = [];
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
datas = _getData();
return Scaffold(
appBar: AppBar(
title: Text('报表'),
),
body: _buildBody(),
);
}
Widget _buildBody() {
return Container(
child: DataGrid(
datas: datas,
fixedKey: 'day',
fixedTitle: '日期',
titleRow: ['今日', '昨日', '前日', '本周', '本月', '本年'],
),
);
}
// 生成数据源
List _getData() {
List datas = [];
for (int i = 0; i < 100; i++) {
Map data = {};
data['day'] = '2020-06-12';
data['today'] = 49899;
data['yesterday'] = 49899;
data['beforeday'] = 49899;
data['week'] = 49899;
data['month'] = 49899;
data['year'] = 49899;
datas.add(data);
}
return datas;
}
}
这样就能实现预览中的效果,读者可以根据实际业务场景调整代码。
注意事项如果是既可以横向滑动又可以纵向滑动的组件必须确定 一个宽度或者高度。Flutter 允许直接设置组件高度或者宽度,也可以通过设置子组件的宽度或者高度来确定组件的宽度或者高度
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)