鸿蒙Flutter花粉监测页面开发:半圆仪表盘与症状追踪
鸿蒙Flutter花粉监测页面开发:半圆仪表盘与症状追踪
前言
健康监测类应用对数据可视化有一个特殊的要求:不仅要呈现当前数值,还要在一眼之内传达"这个数值意味着什么"。对于花粉浓度——342粒/m³这个数字本身对用户毫无意义,但如果说"中等风险,建议佩戴口罩"、并用橙色半圆仪表盘和指针指向中高风险区域,用户的理解速度会提升一个数量级。这就是半圆仪表盘的设计价值:它将0-800粒/m³的连续数值域映射为5段彩色弧线(绿→黄绿→黄→橙→红),指针的位置=风险级别的可视化。本文基于PollenPage的完整代码,在HarmonyOS 7.0平台上介绍如何使用Flutter的drawArc构建分段半圆仪表盘和5级圆点症状追踪系统。
背景
花粉浓度的健康意义是分段阈值式的——50粒以下安全(绿色)、50-100粒轻微(黄绿)、100-200粒中等(黄色)、200-500粒较高(橙色)、500粒以上危险(红色)。这种分段特征天然适合用圆弧仪表的分区弧线呈现——每段弧占据对应浓度范围的角度区间。此外,花粉过敏症患者的日常症状(打喷嚏、流鼻涕、眼痒、咳嗽)需要追踪每日变化趋势——“今天比昨天更严重了还是好转了”,这适合用圆点计数+趋势箭头来表示。同时,不同类型的植物花粉(松/柏/杨/豚草/蒿草)浓度差异需要通过横向卡片列表逐一展示。
Flutter × Harmony7.0 跨端开发介绍
PollenPage在鸿蒙平台上通过_PollenGaugePainter的CustomPainter实现半圆仪表盘渲染。Framework层管理花粉浓度_currentLevel(342粒/m³)和_riskLevel(“中等风险”)两个状态,以及花粉类型列表和症状记录的const数据。Engine层的Skia引擎执行drawArc的5段分区绘制(20%透明度填充分区+完整透明度当前值弧线)+指针的drawLine+drawCircle绘制。Embedder层处理触摸事件。
AOT编译使得drawArc的5次分区调用(每次涉及startAngle和sweepAngle的浮点计算)、指针的cos/sin角度计算和drawLine操作以原生机器码执行。_PollenGaugePainter的shouldRepaint仅在level或color变化时返回true——浓度值不变时仪表盘不重绘,极大的减少了不必要的Canvas操作。_levelColor方法根据浓度值通过if-else链返回对应颜色(<50绿、<100黄绿、<200黄、<500橙、≥500红)。
开发核心代码
第一部分:5段分区半圆仪表盘的drawArc绘制
_PollenGaugePainter的paint方法构建了一个基于180度圆弧(startAngle=π, sweepAngle=π)的半圆仪表盘。背景弧使用16px宽度浅灰色(0xFFF3F4F6)描边+StrokeCap.round,为整个仪表盘提供基础轮廓。5段彩色分区——绿(0-12.5%)、黄绿(12.5-25%)、黄(25-50%)、橙(50-75%)、红(75-100%)——每段通过计算startAngle+seg.$1*sweepAngle和(seg.$2-seg.$1)*sweepAngle精确确定其起始角和扫描角,用20%透明度的对应颜色描边绘制在背景弧之上。
void paint(Canvas canvas, Size size) {
final cx = size.width/2, cy = size.height, radius = size.width*0.42;
final startAngle = math.pi, sweepAngle = math.pi;
canvas.drawArc(Rect.fromCircle(center: Offset(cx,cy), radius: radius),
startAngle, sweepAngle, false, Paint()..color=Color(0xFFF3F4F6)..style=PaintingStyle.stroke..strokeWidth=16..strokeCap=StrokeCap.round);
final segments = [(0.0,0.125,Color(0xFF16A34A)),(0.125,0.25,Color(0xFF84CC16)),(0.25,0.5,Color(0xFFF59E0B)),(0.5,0.75,Color(0xFFF97316)),(0.75,1.0,Color(0xFFEF4444))];
for (final seg in segments) {
canvas.drawArc(Rect.fromCircle(center: Offset(cx,cy), radius: radius),
startAngle+seg.$1*sweepAngle, (seg.$2-seg.$1)*sweepAngle, false,
Paint()..color=seg.$3.withValues(alpha:0.2)..style=PaintingStyle.stroke..strokeWidth=16..strokeCap=StrokeCap.round);
}
// 当前值弧线
final ratio = (level / maxLevel).clamp(0.0, 1.0);
canvas.drawArc(Rect.fromCircle(center: Offset(cx,cy), radius: radius),
startAngle, ratio*sweepAngle, false, Paint()..color=color..style=PaintingStyle.stroke..strokeWidth=16..strokeCap=StrokeCap.round);
// 指针
final needleAngle = startAngle + ratio * sweepAngle;
final needleEnd = Offset(cx+(radius-8)*math.cos(needleAngle), cy+(radius-8)*math.sin(needleAngle));
canvas.drawLine(Offset(cx,cy-8), needleEnd, Paint()..color=Color(0xFF1F2937)..style=PaintingStyle.stroke..strokeWidth=3);
canvas.drawCircle(Offset(cx,cy-8), 5, Paint()..color=Color(0xFF1F2937));
}
指针从圆心偏上8px的旋转中心出发,通过needleAngle = startAngle + ratio*sweepAngle计算当前浓度的角度位置。cos和sin将角度转换为Canvas坐标,指针终点位于radius-8处(避免超出弧线边界)。旋转中心绘制5px半径黑色实心圆作为指针的固定轴。仪表盘下方展示当前浓度的28sp大数字(颜色与浓度级别对应)、风险等级标签(如"中等风险")和行动建议文字(如"过敏体质建议佩戴口罩外出"或"适合户外活动")。
第二部分:5级圆点计数+趋势箭头的症状追踪
症状面板展示4条症状记录(打喷嚏/流鼻涕/眼痒/咳嗽),每条使用Row布局包含:36×36圆角彩色图标+症状名称+5个圆点(填充数=严重程度today值)+趋势箭头。5个圆点通过List.generate(5)生成,每个为14px直径圆形,i<today时填充症状颜色(带同色边框),否则填充浅灰色(0xFFF3F4F6,带灰色边框)。趋势箭头通过today与yesterday的比较决定——today>yesterday显示红色↑(加重)、today<yesterday显示绿色↓(缓解)、相等显示灰色→(持平)。
...List.generate(5, (i) {
return Container(margin: const EdgeInsets.only(right: 3), width: 14, height: 14,
decoration: BoxDecoration(shape: BoxShape.circle,
color: i < today ? color : const Color(0xFFF3F4F6),
border: Border.all(color: i < today ? color.withValues(alpha: 0.3) : const Color(0xFFE5E7EB))));
}),
SizedBox(width: 6),
Text(trend, style: TextStyle(color: trendColor, fontSize: 14, fontWeight: FontWeight.w900)),
4种症状各有独立颜色编码——打喷嚏紫色(0xFF8B5CF6)、流鼻涕青色(0xFF06B6D4)、眼痒金色(0xFFF59E0B)、咳嗽红色(0xFFEF4444)。症状名称使用13sp深绿(0xFF052E16)粗体,图标区域使用对应颜色的8%透明度背景。今日vs昨日的对比标注在标题行右侧,使用绿色半透明文字。
第三部分:花粉类型横向卡片与浓度级别颜色映射
花粉类型列表使用高度100px的横向滚动ListView.separated,5种花粉(松/柏/杨/豚草/蒿草)以130px宽的卡片展示。每张卡片包含植物emoji图标+名称、浓度数值(如"125粒/m³")、风险等级标签。卡片的边框颜色和文字颜色与风险等级对应——中等风险使用金色边框(alpha:0.15),低风险使用绿色边框。_levelColor方法将浓度数值分段映射为5种颜色,该方法在三个位置被调用:圆环颜色、大数字颜色和行动建议文字颜色。
Color _levelColor(int level) {
if (level < 50) return const Color(0xFF16A34A);
if (level < 100) return const Color(0xFF84CC16);
if (level < 200) return const Color(0xFFF59E0B);
if (level < 500) return const Color(0xFFF97316);
return const Color(0xFFEF4444);
}
仪表盘容器使用白色背景+24px圆角+与浓度颜色同色的微阴影(alpha:0.08, blurRadius:16),在视觉上形成仪表盘"浮起于"页面的效果。整体页面配色以健康绿(_pollenPrimary: 0xFF16A34A)为主调,背景色为极浅绿白(0xFFF5FFF7),标题栏显示"北京·朝阳"位置和"更新于10分钟前"的时效性标注。
心得
drawArc的useCenter参数在花粉仪表盘中被设为false——这是有意为之。当useCenter=false时,drawArc仅绘制弧线段而不连接圆心,这使得分区弧线是"弧线段"而非"扇形饼块"。在仪表盘场景中,弧线段更适合表达"刻度区间"的语义——每条彩色弧线都在同一圆周上,视觉上形成连续的刻度带。如果使用useCenter=true,分区弧会变成从圆心辐射的扇形——这更适合饼图的比例展示,但对于仪表盘的"从左到右逐渐危险"的线性叙事而言会产生视觉干扰。
指针的旋转中心偏移(从圆心偏上8px)是一个微小但关键的设计细节。如果旋转中心在圆心(cy),指针需要通过radius向弧线方向延伸——但半圆的圆心在Canvas底边(cy=size.height),指针会从仪表盘底端出发,视觉上显得"针太长"。偏上8px使旋转中心更接近弧线的圆心位置——虽然从几何上说不是严格同心,但在120px半径的尺度下8px的偏差几乎不可察觉,而指针长度的缩短使整体比例更协调。
症状追踪系统中的5级圆点计数法是一种"离散量可视化"的经典实现。5个圆点承载了0-5的严重程度(但实际today值仅为0-3),比数字"3次"更直观——用户一眼就能看到"3个实心圆+2个空心圆=症状中等"。趋势箭头使用了up/down/right三种Unicode箭头符号(↑↓→)而非自定义图标,这种Unicode符号方案比引入图标库更轻量,且在任何字体环境下都能正确显示。
在鸿蒙适配方面,PollenPage的Canvas仪表盘绘制完全跨平台一致。_levelColor的if-else链在实际部署中可能需要接入花粉浓度API的动态数据——当前的硬编码342粒/m³仅作为演示值。Platform Channel可用于从鸿蒙原生侧发起HTTP请求获取实时浓度数据并回传Flutter。
总结
本文以PollenPage为完整案例,展示了在HarmonyOS 7.0上使用Flutter构建花粉监测页面的实现方案。核心技术包括:5段drawArc分区+指针的半圆仪表盘渲染、_levelColor的5级if-else阈值颜色映射、5级圆点计数+趋势箭头(↑↓→)的症状追踪系统、以及花粉类型横向滚动卡片的浓度→颜色视觉联动。仪表盘的5段分区弧线和指针通过Canvas自绘实现,所有几何计算基于startAngle/sweepAngle的三角函数映射。
花粉监测页面的设计揭示了健康数据可视化的核心原则:数字本身没有意义,有意义的是数字在"风险级别"坐标轴上的位置。342粒/m³这个数字本身不会促使用户采取行动,"中等风险,橙色指针超过一半"会。这就是数据可视化的底层逻辑——将抽象的数值映射到人类视觉系统能够快速识别的位置/颜色/大小的变化上。半圆仪表盘的弧形刻度天然适配这种"从左到右从安全到危险"的线性叙事,而Flutter的drawArc提供了实现这一叙事的精确几何控制能力。
更多推荐





所有评论(0)