
接手鸿蒙版天气应用的图表重构之前我一直以为“画个折线图”是最不值得讨论的需求。但真当需求落到面前——温度要能看出昼夜趋势降水量要能一眼分辨哪个时段有雨湿度要能和温度对照着看还要在鸿蒙设备上跑得流畅——我才发现这事远比想象中复杂。第一版我尝试用自绘Canvas把所有元素从零画出来坐标轴、刻度、网格、手势、Tooltip全手搓折腾了两周效果依然不理想。后来换成Flutter生态里的fl_chart三天就上了线还顺手解决了动画和触摸交互。这篇文章我把自己在选型、分图、接入、调优和踩坑上的完整思路整理出来给准备做同类天气数据可视化或者正在鸿蒙应用里做Flutter图表层的朋友当一个参考。1. 鸿蒙版天气应用为什么绕回来用Flutter画图表1.1 第一个被否掉的方案自绘Canvas最开始看到鸿蒙版天气应用的需求我脑子里冒出来的方案是直接走原生自绘。平台的原生绘制能力确实够强理论上什么都能画但问题是你需要画的不是一个静态图而是带坐标轴、动态刻度、触摸回调、Tooltip、切换动画的交互图表。如果走自绘我要自己维护的东西会非常长。横向的日期刻度怎么根据屏幕宽度自动抽稀纵向的区间怎么让温度在-5℃到40℃之间显示得合理手指按到折线附近时怎么算出最近的采样点并弹出提示城市切换时新旧数据怎么补间动画这些问题每一个都不难但加起来就是一个完整图表库的工作量。为了一个天气页面去重造这些轮子性价比太低了。1.2 fl_chart的定位恰好符合“够用且可控”后来我转向了Flutter图表库主要看中三件事第一Flutter图表代码在鸿蒙端可以直接嵌入运行。这个项目本身有跨端需求未来其他端也要用同一套UI用Flutter画图表能让视觉和交互保持一致不至于每端重写一遍。第二fl_chart对常用图表类型的覆盖非常精准。它提供折线图、柱状图、饼图、散点图等天气场景需要的就集中在这几种不用引入一个庞大复杂的商业图表组件库体积和定制成本都可控。第三动画和触摸是现成的。fl_chart内置了数据变化时的动画过渡和触摸回调。这个在自绘方案中非常耗时在它这里是默认能力。我也不太建议一上来就追全功能图表方案很多功能用不上反而增加包体积和版本冲突风险。fl_chart的代码量适中读得懂源码真遇到问题改起来也快。1.3 一个关键判断图表库选型要跟着数据特征走选型之前我先列了一张表把三种数据和候选图型做了一次匹配数据数据类型关注重点适合图型温度连续值昼夜变化趋势、极值折线图降水量离散事件有雨没雨、峰值大小柱状图湿度百分比与温度对照的干湿变化折线图/双Y轴用这个表能看到真正制约选型的不是“哪个图更漂亮”而是数据本身的属性。温度的连续特性决定了它用折线表达趋势最自然降水的离散特性用柱状高度来对比“量级”最直观湿度作为一个百分比指标放在第二Y轴能跟温度在同一时间轴下对比。想清楚这一点之后fl_chart里该用哪两个组件就已经明确了。2. 图表不是越炫越好三种天气数据该怎么拆2.1 温度连续变量折线图是唯一合理选项温度是典型的连续变量。从早上到夜里温度的变化是平滑渐进的过程不会在某个小时突然从20℃跳到30℃。这种数据要表达的语义是“趋势”而折线图恰恰是把点的位置用时间轴串联让上升、下降、波动的过程一目了然。我一开始也想过用柱状图展示每小时温度。画出来之后发现柱子的高度虽然能看出数值高低但“趋势”被柱子之间的割裂感削弱了尤其是一天中温度曲线来回波动的部分用户不容易第一时间看出来“夜里一直在降温”。换成折线图之后视觉通道从“逐个比较高度”变成了“扫视整体走向”信息读取效率明显提升。在fl_chart里折线图对应LineChart组件核心是把时间点和温度值转成FlSpot。比如早上8点24.5℃生成FlSpot(8, 24.5)。FlSpot的x是时间索引y是温度值单位统一用摄氏度保存展示时再决定是否带小数。2.2 降水量离散事件柱状图更能体现“有没有、有多大”降水量的数据特征和温度完全不同。它不是一条连续变化的线而是“某段时间有没有下雨、下多大”。用柱状图展示每个小时一根柱子柱高代表雨量零降水时柱子高度为0一眼就能看到哪几个时段降雨集中。这里有一个我自己栽过跟头的点降水量不适合用折线图。如果把有雨时段和无雨时段用折线连起来曲线会在0值附近来回上下穿梭视觉上似乎暗示“雨量在平滑变化”但实际并没有这样的物理意义。用户在查看小时降雨时更想知道的是“这一小时有没有下”和“哪一小时雨最大”柱状图的离散块状表达正好匹配这个心理模型。在fl_chart里柱状图对应BarChart组件。每个小时一组数据用BarChartGroupData包裹柱子高度用BarChartRodData的toY表示。再把相邻柱子的间距调小一点视觉上就会有一个“雨量随时间分布”的整体感。2.3 湿度百分比指标与温度共用一张双Y轴图湿度本质上也是连续变量但它和温度有个明显的差异量纲不同。温度在-10℃到40℃之间波动湿度在0%到100%之间波动如果都放在同一个Y轴数值小的那一方会被压成一条几乎水平的线。我最后采用的是“温度湿度双Y轴组合图”。温度走左侧Y轴湿度走右侧Y轴。因为fl_chart的LineChart默认只支持一个数据坐标系但可以通过右侧标题配置来模拟第二个Y轴把湿度值映射到右侧刻度区间再给湿度折线指定一个不同的颜色和透明度这样两条线在时间轴上完全对齐又能区分各自的量纲。老实说双Y轴不是必须的。如果你只有一个竖屏单列卡片温度和湿度分开两张图放上下排列也完全可行。但我在实际使用中发现用户看天气时往往想同时感知“温度在升、湿度在降”这种关联变化共用一张双Y轴图能把这种关联性更紧凑地表达出来。3. 接入fl_chart三步把它跑起来3.1 依赖与版本不要随便追最新在项目的pubspec.yaml里添加依赖dependencies: fl_chart: ^0.66.0版本号我建议选一个稳定的大版本不需要追求最新。fl_chart迭代不算慢大版本之间偶尔会有API命名调整比如部分配置项从oldStyle迁移到新写法。如果你的项目还用了其他Flutter图表组件要注意依赖树是否冲突。我实际接入时发现只要Flutter版本不低于项目整体要求这个库的兼容性还比较省心。在鸿蒙设备上跑还有一个建议优先使用当前稳定的Flutter SDK版本。图表组件对渲染管线没有太多特殊依赖但跨端工程通常已经集成了很多插件保持Flutter版本稳定能减少很多莫名其妙的编译问题。3.2 第一个图表温度折线先看一个最精简的温度折线图配置LineChart( LineChartData( minY: 0, maxY: 40, gridData: FlGridData( show: true, drawVerticalLine: false, ), borderData: FlBorderData(show: false), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, getTitlesWidget: (value, meta) Text(${value.toInt()}°), ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) Text(_hourLabel(value.toInt())), ), ), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), ), lineBarsData: [ LineChartBarData( spots: _tempsSpots, isCurved: true, colors: [Color(0xFF3B82F6)], barWidth: 2.5, dotData: FlDotData(show: false), belowBarData: BarAreaData( show: true, color: Color(0xFF3B82F6).withValues(alpha: 0.1), ), ), ], ), )这段配置有几个地方值得注意minY和maxY不要直接依赖数据自动范围。天气图表如果是从0到40固定区间用户就能凭直觉判断当前温度大概处于一天中的什么水平如果每次数据一来区间就跟着变反而会误导用户对冷暖的感知。isCurved: true让折线变得平滑符合温度连续变化的物理直觉。belowBarData给曲线下方加一个低透明度的填充这是天气类应用很通用的做法让趋势图更有“面积感”。数据转换部分是这样的ListFlSpot _buildTempSpots(ListHourlyWeather hours) { return List.generate(hours.length, (i) { final temp hours[i].temperature; return FlSpot(i.toDouble(), double.parse(temp.toStringAsFixed(1))); }); }这里有个细节toStringAsFixed(1)把温度限制到一位小数避免因为浮点误差在图表左侧显示一堆乱七八糟的数字。我们在测试时见过22.300000000000004这种值有的版本会把Y轴刻度渲染成丑陋的浮点长尾巴。3.3 降水柱状图与湿度曲线的接入降水柱状图的配置比折线图更直接BarChart( BarChartData( maxY: 30, alignment: BarChartAlignment.spaceAround, gridData: FlGridData(show: true), borderData: FlBorderData(show: false), barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIndex, rod, rodIndex) { final mm rod.toY; return BarTooltipItem( ${group.x}时\n${mm.toStringAsFixed(1)}mm, const TextStyle(color: Colors.white), ); }, ), ), barGroups: List.generate(hours.length, (i) { final precip hours[i].precipitation; return BarChartGroupData( x: i, barRods: [ BarChartRodData( toY: precip, width: 6, borderRadius: BorderRadius.circular(2), color: Color(0xFF38BDF8), ), ], ); }), ), )alignment: BarChartAlignment.spaceAround让每个小时的柱子居中对齐看起来更整齐。降水量单位是毫米柱子的toY直接取原值。0降水时柱子高度也确实是0这个后面会专门讲怎么处理“零值消失”的观感问题。湿度曲线的接入和温度折线几乎一样区别只在两条线共用一个LineChart。代码上把温度线放在lineBarsData的第一个元素湿度线放在第二个元素右侧标题单独显示百分比rightTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 44, getTitlesWidget: (value, meta) Text(${value.toInt()}%), ), ),湿度线的颜色我用浅橙色透明度降到70%左右这样和主温度线叠在一起时不会抢视觉重点。两条线如果都用同样的颜色和粗细用户会很难分清哪条是温度哪条是湿度这是组合图最容易犯的错。4. 真正耗时的是“好看好用”的打磨4.1 触摸和Tooltip数字要跟手图表接入跑通之后我花了最多时间的不是画数据而是把Tooltip体验磨到满意。天气图表有个特点数据点密集24个小时温度折线和每小时降水柱状图靠视觉看只能掌握“大概”用户想了解具体时刻的数值很自然会去触摸图表。fl_chart的触摸体验默认已经不错但有几个参数值得仔细调。一个是LineTouchData的回调。我想让用户触摸折线附近时自动把最近的采样点高亮并显示对应小时和温度。实现上是用touchCallback拿到FlTouchEvent和LineTouchResponse从response?.touchedSpotIndex取出最新的索引再更新当前选中状态lineTouchData: LineTouchData( touchCallback: (event, response) { if (response null || response.touchedSpotIndex null) return; final index response.touchedSpotIndex!; setState(() _selectedHourIndex index); }, touchTooltipData: LineTouchTooltipData( getTooltipColor: (touchedSpot) Colors.black87, getTooltipItems: (touchedSpots) { return touchedSpots.map((spot) { final index spot.x.toInt(); return LineTooltipItem( ${_hourLabel(index)} ${spot.y.toStringAsFixed(1)}°, TextStyle(color: Colors.white), ); }).toList(); }, ), ),还有一点Tooltip默认会出现在手指上方当手指滑到屏幕最右侧时Tooltip的文字很容易超出屏幕边缘被截断。我用的处理方式是给图表外层的容器在右侧留出一段安全边距同时限制Tooltip文字宽度必要时换行。这个看着是小问题但用户实际使用时会频繁遇到。4.2 动画与刷新策略别让重绘拖垮滚动天气图表在进入页面和切换城市时都会经历一次数据更新。fl_chart是带有动画特性的图表组件配置Duration后新旧数据之间会自动补间从旧曲线平滑过渡到新曲线。这个动画效果我很推荐保留因为它能给用户一个视觉上的“定位”过程而不是让数据突然跳变。但动画也有代价。如果页面里同时有折线图、柱状图、湿度曲线而且父组件每次滚动都触发setState图表会频繁重绘在鸿蒙设备的中低端机型上会明显卡顿。我的优化策略有三点第一把图表数据计算和图表组件本身分离。原始数据转换成FlSpot列表、生成BarGroups这些操作只在实际数据变化时执行结果存到State里不每次都重新计算。第二图表数据不变时用const构造图表。如果数据已经确定const LineChart(LineChartData(...))可以减少重复构建。如果必须传入动态变量在父组件里用shouldRepaint逻辑手动拦截数据没变化就返回false。第三控制点数和装饰项的渲染开销。FlSpot数量按每小时一个点足够不需要分钟级。折线图的点如果不需要托盘可以设置FlDotData(show: false)。网格线和刻度的透明度降低一些也能明显减少绘制压力。4.3 在鸿蒙设备上的实际帧率与内存观察我在鸿蒙设备的低配机型上做过简单实测。单独的LineChart或BarChart10秒动画期间帧率稳定在55到60帧。页面里同时挂三个图表组件并且上下滚动时帧率会有所下降但视觉上没有明显掉帧。比较明显的问题是内存。每个图表组件内部都有独立的Painter和动画控制器如果页面频繁进出或者在不必要的时候重建整个图表树内存会快速上涨。我的规避办法是在页面销毁时把图表组件从树中移除并且不持有多余的动画控制器引用。进入页面时用animateOnAppear播放入场动画退出时直接销毁不保留后台动画任务。5. 两周实测下来的坑至少能省你一个周末5.1 刻度小数失控22.300000001℃现象出现在第一版上线后。温度折线图左侧的Y轴刻度偶尔会出现一串很长的小数比如22.300000000000004。坐标轴上出现这种数字视觉上非常业余用户也会直观觉得数据不可靠。排查链路大概是这样的首先怀疑是数据源的问题打印接口返回的原始温度和FlSpot里的y值发现接口数据是正常的浮点数转换后也确实保留了精度。继续调试后发现根因是fl_chart在计算坐标轴刻度时会基于数据范围做插值。当minY和maxY是直接根据数据计算出来的浮点边界时内部插值产生的刻度值就会带上浮点噪声。解决办法是显式规定坐标轴边界和刻度间隔。温度图固定minY为0、maxY为40Y轴刻度按5度一步生成这样刻度值永远都是整数。如果业务上确实需要动态边界就把边界值用roundToDouble()先取整避免不规则小数。5.2 柱状图无缝对应但零值“消失”了降水柱状图接入后遇到一个观感问题某一天完全没有降雨整排柱子全部高度为0页面上只留下一根根看不见的柱子在时间位置上“凭空对齐”视觉上非常奇怪。我先试着把柱子的最小高度设为1结果1mm的“视觉雨量”反而误导用户看起来似乎下过雨。反复调整后采用的方案是零值时画一根低透明度、只有格线高度的极细占位柱同时在柱状图上方用一个图标加文字表示“今日无降水”。占位柱的透明度很低用户不会误读成降雨但能保持时间轴的连续性。5.3 Tooltip在屏幕边缘被截断这是触摸体验里最容易被忽略的问题。手指滑到图表最右侧时Tooltip默认出现在手指上方偏右的位置文字很容易超出屏幕边界。解决方式比较朴素在FlTitlesData里为右边距预留更多空间同时限制Tooltip文字的最大宽度超过最大宽度就换行。组合图表里两个Tooltip项并排出现文字一长很容易撞在一起我给Tooltip背景设置了较深的半透明色并给文字设置了白底色反差这样即使重叠也能看清内容。5.4 热重载后图表错乱别信热重载的“暂时性”开发阶段最坑的一件事修改代码保存后热重载图表页面有时会变成一片空白或者坐标轴和折线错位看起来像数据没传进去。一开始我以为是fl_chart的Bug反复检查后发现这是热重载后旧图表实例仍在执行动画导致的竞态。解决办法是避免用热重载后的瞬时状态判断问题。如果图表刷新异常先冷启动应用看一次通常冷启动一切正常。开发策略上我把图表组件的创建放在一个有ValueKey的子树里数据源变化时通过重建Key强制重新挂载图表保证动画状态和数据源始终同步。5.5 小屏设备的排版挤压在鸿蒙设备的小屏机型上温度和湿度双Y轴组合图会遇到左侧和右侧刻度文字互相挤压的问题。左边要显示温度“24°”右边要显示湿度“62%”中间还要留足折线绘制区域屏幕一窄整个图表就显得特别局促。我最后通过三个调整解决了左侧Y轴reservedSize设为40右侧设为44给刻度文字足够的空间底部的时间刻度只显示偶数小时减少文字重叠折线图左右两侧不用边框用网格背景替代省下不少像素。如果屏幕实在太小我建议放弃双Y轴直接上下排列温度和湿度两张图。牺牲一点关联性换来的却是一目了然的可读性这在移动端场景里更值得。说回这几天折腾下来的整体感受用fl_chart做天气图表最难的不是把线画出来而是理解数据该以什么样的形式被用户读取。折线图要留住温度的趋势感柱状图要尊重降水的离散性双Y轴要处理好湿度与温度的量纲差异。这些想清楚了fl_chart就是一个趁手的工具配置起来很快动画和触摸也够成熟。我自己的习惯是每次拿到新图表需求先画一版最简单的静态图确认数据映射无误再一点点加网格、填充、动画这些装饰。最早动手画折线图时不要追求一步到位从一个能看清数值的LineChart开始后面的事情都不会太难。