鸿蒙Flutter潮汐预报页面开发:贝塞尔曲线与海水填充
鸿蒙Flutter潮汐预报页面开发:贝塞尔曲线与海水填充
前言
潮汐数据的可视化面临一个经典的"稀疏数据+连续呈现"难题:每天只有4个关键观测点(两次满潮、两次干潮),但用户期望看到的是0-24小时的连续潮位变化曲线。如果将这4个点用直线连接,画面呈现出机械的锯齿状折线,完全背离了潮汐作为自然现象应有的平滑特征。解决方案是贝塞尔曲线插值——在4个数据点之间插入二次贝塞尔控制点,利用贝塞尔曲线的平滑特性在数据不足的情况下"补全"连续感。配合曲线下方的蓝色渐变填充模拟海水深度,使潮汐图从"折线图"升华为"海洋剖面图"。本文基于TidePage的完整代码,在HarmonyOS 7.0平台上介绍Canvas潮汐曲线和海水填充的实现方法。
背景
潮汐预报页面的信息架构分为四个层次:第一层是当前潮位状态横幅——展示正在涨潮或退潮的状态、当前水位、距下一次转折点的时间;第二层是24小时潮位曲线——横轴0-24小时、纵轴潮位高度、标注满潮/干潮转折点;第三层是当日四点数据——满潮(蓝色)和干潮(金色)的时间和高度数值;第四层是未来天预测——日期+满潮干潮时间+潮差柱状图。这种从"当前状态→完整曲线→详细数据→未来趋势"的递进式信息架构符合用户从宏观到微观的阅读逻辑。
Flutter × Harmony7.0 跨端开发介绍
TidePage在鸿蒙平台上通过_TideCurvePainter的CustomPainter实现潮汐曲线的Canvas渲染。Framework层管理潮汐数据——_todayTides(4个转折点)和_weekTides(4天预测)以const List
AOT编译使得paint方法中的网格线绘制、数据点坐标计算、贝塞尔曲线Path构建和渐变shader创建全部以原生机器码执行。_TideCurvePainter的shouldRepaint始终返回false——潮汐数据在当前会话中不变,曲线仅需绘制一次,后续帧无需重绘。这极大地降低了Canvas的持续GPU开销。const潮汐数据在编译期分配,时间字符串的split解析在paint方法中仅执行一次。
开发核心代码
第一部分:数据点坐标映射与网格线绘制
paint方法的首步是将四条潮汐数据的时间字符串和高度数值映射为Canvas坐标。时间解析为hour+min/60,通过(hour+min/60)/24.0w映射为0-width之间的X坐标;潮位高度通过h-(height/5.0h)映射为Y坐标(5.0为参考最高潮位,Y轴向上倒转)。四个Offset值存入points列表。网格线使用0.5px浅灰横向线条绘制4条水平参考线(对应1.25m/2.5m/3.75m/5.0m潮位),为曲线提供高度参照系。
final points = <Offset>[];
for (final t in tides) {
final hour = int.parse((t['time'] as String).split(':')[0]);
final min = int.parse((t['time'] as String).split(':')[1]);
final x = (hour + min / 60) / 24.0 * w;
final y = h - ((t['height'] as double) / maxH * h);
points.add(Offset(x, y));
}
仅4个原始数据点不足以覆盖整条曲线的起点(X=0处)和终点(X=w处)。为此在points前后各加一个镜像点——前方加入Offset(0, points.last.dy)使曲线在0时处与最后一个数据点等高,后方加入Offset(w, points.first.dy)使曲线在24时处与第一个数据点等高。这个镜像策略基于潮汐的自然连续性——今天的最后一次潮位与明天的第一次潮位(即24小时后)通常接近——使曲线首尾平滑相接。
第二部分:二次贝塞尔曲线Path构建与海水渐变填充
Path的构建以moveTo(0, h)从底部左端开始,lineTo到第一个镜像数据点的Y高度,然后进入循环——对每对相邻数据点(p0, p1),用quadraticBezierTo以p0为控制点、以两点终点为终点绘制二次贝塞尔曲线段。循环结束后lineTo到右端镜像数据点、再lineTo到(w, h)右下角底部、close闭合。这个Path构成了从底部→曲线→底部→闭合的完整多边形。
final allPoints = [Offset(0, points.last.dy), ...points, Offset(w, points.first.dy)];
final path = Path()
..moveTo(0, h)..lineTo(0, allPoints[1].dy);
for (int i = 1; i < allPoints.length - 2; i++) {
final p0 = allPoints[i], p1 = allPoints[i + 1];
path.quadraticBezierTo(p0.dx, p0.dy, (p0.dx + p1.dx) / 2, (p0.dy + p1.dy) / 2);
}
path.lineTo(w, allPoints[allPoints.length - 2].dy)..lineTo(w, h)..close();
// 海水填充:顶部30%透明度到底部5%透明度的蓝色渐变
canvas.drawPath(path, Paint()..shader = LinearGradient(
colors: [Color(0xFF0077B6).withValues(alpha: 0.3), Color(0xFF0077B6).withValues(alpha: 0.05)],
begin: Alignment.topCenter, end: Alignment.bottomCenter).createShader(Rect.fromLTWH(0, 0, w, h)));
// 曲线描边
canvas.drawPath(path, Paint()..color = _tidePrimary..style = PaintingStyle.stroke..strokeWidth = 2.5);
海水填充使用LinearGradient的shader,从顶部30%透明度到低部5%透明度的蓝色渐变——视觉上模拟了海水越深越暗的自然现象。曲线描边使用2.5px宽度的深蓝色(_tidePrimary: 0xFF0077B6)描边Path。转折点使用双层圆点标记——外层5px白色圆点+内层4px蓝色(满潮)或金色(干潮)圆点。TextPainter在上方标注时间和高度,排版后通过p.dx-label.width/2和p.dy-22居中定位。
第三部分:潮位状态横幅与周概览潮差柱状图
当前潮位横幅使用深海蓝到天蓝的对角线渐变背景,左侧64×64圆形区域内嵌AnimatedBuilder驱动的海浪emoji——大小通过sin(_waveCtrl.value*2π)*4在28±4sp之间波动,模拟海浪的起伏。中间展示"正在涨潮"文字+trending_up图标 + 当前潮位以及距满潮剩余时间,底部展示"适合钓鱼"和"适合冲浪"两个活动标签。
AnimatedBuilder(animation: _waveCtrl, builder: (_, __) {
return Container(width: 64, height: 64, decoration: BoxDecoration(shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.15)), alignment: Alignment.center,
child: Text('🌊', style: TextStyle(fontSize: 28 + math.sin(_waveCtrl.value*2*math.pi)*4)));
})
周概览使用纵向Column排列4天预测数据,每天一行Row布局:日期列(50px宽,日期+星期)→潮差柱状条(8px宽×60px高,barHeight = (range/4.0)*60映射)→满潮干潮时间列(Flexible弹性宽度)→潮差值。潮差柱状条的填充使用深蓝到天蓝的纵向渐变,蓝色高度的数值大小直观反映当天的潮差——4.1m的潮差柱状条几乎填满60px高容器,2.7m的只填约40px。_SmallTag辅助方法使用3px圆角+极低透明度底色(alpha:0.12)生成满潮"满"和干潮"干"两个小巧的标签。
心得
贝塞尔曲线在稀疏数据插值中的应用让我认识到"控制点的几何意义"在自然可视化中的重要性。quadraticBezierTo需要一个控制点和一个终点——控制点决定了曲线在起点和终点之间的弯曲程度和方向。在这段代码中,控制点设为当前数据点p0、终点设为相邻两点中点(p0+p1)/2——这样的设计使曲线在数据点附近弯向该数据点的位置,在两点之间平滑过渡,避免了在数据点处出现尖锐的转折。如果终点直接设为p1而不是中点,曲线会在两个数据点之间产生更深的弯曲,可能使潮位曲线出现不真实的高峰或低谷。
海水渐变填充的透明度选择(中透明度30%到底部5%)经过反复调整。顶部透明度太高(如50%)会使曲线下方的蓝色过于浓重,遮盖网格线的参考作用;底透明度太低(如1%)会使水域部分与白色背景几乎不可区分。30%到5%的梯度在保证曲线下方区域被认定为"水域"的前提下,保持了网格线和数字的可读性。这种"半透明填充+实体描边"的双层渲染模式是折线图/面积图在Canvas中的经典实现范式。
潮差柱状条的设计将抽象的"潮差数值"转化为可直接比较的视觉量。4天的潮差数值(3.7/3.5/3.2/2.7米)直接通过barHeight=(range/4.0)*60映射为px高度——4.0是参考最大潮差,60是柱状容器的高度。用户无需阅读数值就能通过视觉大小感知"周一的潮差最大、周四的最小"。这种"数据→视觉量"的直接映射是数据可视化的核心价值,在移动端小屏幕场景中尤为重要。
在鸿蒙适配方面,TidePage的Canvas渲染完全跨平台一致。潮汐数据目前为硬编码的示例数据,实际应用需要接入潮汐预报API(如全球潮汐模型TPXO或中国近海潮汐数据)。通过Platform Channel可以在鸿蒙原生侧发起网络请求获取JSON数据,解析后回传Flutter侧更新_todayTides和_weekTides。
总结
本文以TidePage为完整案例,展示了在HarmonyOS 7.0上使用Flutter构建潮汐预报页面的实现方案。核心技术包括:四点数据到24小时连续曲线的镜像扩展+二次贝塞尔插值算法、LinearGradient shader的海水渐变填充(30%→5%透明度递减)、双层圆点标记+TextPainter标注的转折点视觉设计、以及潮差比例映射的柱状对比图。所有Canvas绘制在创建时执行一次(shouldRepaint返回false),后续页面滚动和交互不涉及重复的Canvas渲染。
潮汐页面的数据可视化设计体现了一个被忽视的技术点:自然观测数据呈现的核心不是数值的精确性(用户不需要知道水位是3.826m还是3.831m),而是变化的"趋势感"——涨潮中还是退潮中、潮差大还是小、满潮在几点。蓝色填充的海水区域在曲线下方的面积变化直观传达了"涨潮→潮水增多、退潮→潮水减少"的动态视觉隐喻,这种基于视觉面积的信息传递比数字阅读快一个数量级,符合移动端用户快速获取信息的核心需求。
更多推荐





所有评论(0)