如何在Flutter 3.3.9中定制fl_chart 0.61.0以匹配Figma设计
定制fl_chart 0.61.0匹配Figma设计方案
目标设计图

需求:将fl_chart 0.61.0定制为与上述设计一致,使用Flutter版本3.3.9,以下是当前实现代码:
import 'package:flutter/material.dart'; import '../app_color.dart'; import 'chart/fl_chart.dart'; class AppLineChart extends StatefulWidget { final List<dynamic> dataList; final String? direction; final double? maxY; const AppLineChart({Key? key, required this.dataList, this.direction, this.maxY}) : super(key: key); @override State<AppLineChart> createState() => _AppLineChartState(); } class _AppLineChartState extends State<AppLineChart> { List<Color> gradientColors = [ const Color(0xff23b6e6), const Color(0xff02d39a), ]; List<FlSpot> flSpots = []; bool isAvg = true; @override void initState() { super.initState(); for (int i = 0; i < widget.dataList.length; i++) { flSpots.add( FlSpot(i.toDouble(), widget.dataList[i]), ); } Future.delayed(const Duration(milliseconds: 500), () { setState(() { isAvg = false; }); }); } @override Widget build(BuildContext context) { return AspectRatio( aspectRatio: 1.7, child: DecoratedBox( decoration: const BoxDecoration( borderRadius: BorderRadius.all( Radius.circular(18), ), color: AppColor.backgroundColor, ), child: Padding( padding: const EdgeInsets.only( right: 48, left: 48, top: 24, bottom: 24, ), child: LineChart(isAvg ? avgData() : mainData()), // child: LineChart(avgData()), ), ), ); } Widget bottomTitleWidgets(double value, TitleMeta meta) { const style = TextStyle( color: AppColor.white, fontWeight: FontWeight.w500, fontFamily: 'SVN-Gilroy', fontSize: 16, ); Widget text; switch (value.toInt()) { case 0: text = const Text('00', style: style); break; case 6: text = const Text('06', style: style); break; case 12: text = const Text('12', style: style); break; case 18: text = const Text('18', style: style); break; case 23: text = const Text('24', style: style); break; default: text = const Text('', style: style); break; } return SideTitleWidget( axisSide: meta.axisSide, child: text, ); } LineChartData mainData() { return LineChartData( gridData: FlGridData( show: false, drawVerticalLine: true, horizontalInterval: 1, verticalInterval: 1, getDrawingHorizontalLine: (value) { return FlLine( color: const Color(0xff37434d), strokeWidth: 2, ); }, getDrawingVerticalLine: (value) { return FlLine( color: const Color(0xff37434d), strokeWidth: 2, ); }, ), titlesData: FlTitlesData( show: true, rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, interval: 1, getTitlesWidget: bottomTitleWidgets, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: false, ), ), ), borderData: FlBorderData( show: false, border: Border.all(color: const Color(0xff37434d)), ), minX: 0, maxX: 24, minY: 0, maxY: widget.maxY == null ? 42 : widget.maxY!, lineBarsData: [ LineChartBarData( spots: flSpots, isCurved: true, gradient: LinearGradient( colors: gradientColors, ), barWidth: 1.5, isStrokeCapRound: false, dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { if (index == DateTime.now().hour) { return FlDotCirclePainter( radius: 4, color: gradientColors[0], strokeWidth: 2, strokeColor: gradientColors[0]); } else { return FlDotCirclePainter( radius: 0, color: gradientColors[0], strokeWidth: 2, strokeColor: Colors.transparent, ); } }, ), belowBarData: BarAreaData( show: false, gradient: LinearGradient( colors: gradientColors.map((color) => color.withOpacity(0.3)).toList(), ), ), ), ], ); } LineChartData avgData() { return LineChartData( lineTouchData: LineTouchData(enabled: false), gridData: FlGridData( show: false, drawHorizontalLine: true, verticalInterval: 1, horizontalInterval: 1, getDrawingVerticalLine: (value) { return FlLine( color: const Color(0xff37434d), strokeWidth: 2, ); }, getDrawingHorizontalLine: (value) { return FlLine( color: const Color(0xff37434d), strokeWidth: 2, ); }, ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, getTitlesWidget: bottomTitleWidgets, interval: 1, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: false, ), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData( show: false, ), minX: 0, maxX: 23, minY: 0, maxY: widget.maxY == null ? 42 : widget.maxY!, lineBarsData: [ LineChartBarData( spots: const [ FlSpot(0, 10), FlSpot(1, 10), FlSpot(2, 10), FlSpot(3, 10), FlSpot(4, 10), FlSpot(5, 10), FlSpot(6, 10), FlSpot(7, 10), FlSpot(8, 10), FlSpot(9, 10), FlSpot(10, 10), FlSpot(11, 10), FlSpot(12, 10), FlSpot(13, 10), FlSpot(14, 10), FlSpot(15, 10), FlSpot(16, 10), FlSpot(17, 10), FlSpot(18, 10), FlSpot(19, 10), FlSpot(20, 10), FlSpot(21, 10), FlSpot(22, 10), FlSpot(23, 10), ], isCurved: true, gradient: LinearGradient( colors: [ ColorTween(begin: gradientColors[0], end: gradientColors[1]).lerp(0.2)!, ColorTween(begin: gradientColors[0], end: gradientColors[1]).lerp(0.2)!, ], ), barWidth: 1.5, isStrokeCapRound: true, dotData: FlDotData( show: false, ), belowBarData: BarAreaData( show: false, ), ), ], ); } }
当前输出效果

解决方案
对比目标设计与当前实现的差异,以下是针对性的代码修改:
1. 开启线条下方渐变填充
目标设计中线条到X轴之间有半透明渐变填充,修改mainData()里的belowBarData:
belowBarData: BarAreaData( show: true, gradient: LinearGradient( colors: gradientColors.map((color) => color.withOpacity(0.2)).toList(), begin: Alignment.topCenter, end: Alignment.bottomCenter, ), borderColor: Colors.transparent, ),
2. 调整线条视觉样式
- 增加线条宽度匹配设计图
- 设置线条两端为圆角
barWidth: 3, // 从1.5调整为3 isStrokeCapRound: true, // 从false改为true
3. 显示所有数据点并区分当前时间点
目标设计中所有数据点都显示为小圆点,当前时间点高亮显示,修改dotData:
dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { if (index == DateTime.now().hour) { return FlDotCirclePainter( radius: 5, color: Colors.white, strokeWidth: 2, strokeColor: gradientColors[0], ); } else { return FlDotCirclePainter( radius: 3, color: gradientColors[0], strokeWidth: 0, ); } }, ),
4. 优化底部刻度与竖线
目标设计中刻度上方有短竖线,修改bottomTitleWidgets并调整底部标题配置:
Widget bottomTitleWidgets(double value, TitleMeta meta) { const style = TextStyle( color: AppColor.white, fontWeight: FontWeight.w500, fontFamily: 'SVN-Gilroy', fontSize: 16, ); Widget text; switch (value.toInt()) { case 0: text = const Text('00', style: style); break; case 6: text = const Text('06', style: style); break; case 12: text = const Text('12', style: style); break; case 18: text = const Text('18', style: style); break; case 23: text = const Text('24', style: style); break; default: return const SizedBox(); } return Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 2, height: 8, color: AppColor.white.withOpacity(0.5), ), const SizedBox(height: 4), text, ], ); }
同时更新bottomTitles配置:
bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, // 增加预留空间容纳竖线和文字 interval: 6, // 直接设置间隔为6,减少不必要的刻度处理 getTitlesWidget: bottomTitleWidgets, ), ),
5. 移除网格线
设计图无网格线,修改mainData()和avgData()里的gridData:
gridData: const FlGridData( show: false, ),
6. 调整容器内边距
适配新的刻度布局,修改Padding:
padding: const EdgeInsets.only( right: 24, left: 24, top: 16, bottom: 32, // 增加底部内边距 ),
7. 修正X轴范围
将mainData()里的maxX从24改为23,匹配24个小时点的索引范围:
maxX: 23,
内容的提问来源于stack exchange,提问作者PNT
相关产品推荐
相关产品推荐

