【共创季稿事节】会呼吸的天气球:HarmonyOS 6.1 Canvas 2D 高帧率动效与 AI 天气体验实战

【共创季稿事节】会呼吸的天气球:HarmonyOS 6.1 Canvas 2D 高帧率动效与 AI 天气体验实战

在这里插入图片描述

一个天气应用最容易陷入的误区,是把信息做得越来越多:温度、湿度、风速、预警、空气质量、生活指数依次堆叠,最后用户打开页面,却仍然不知道“我现在该做什么”。

本文以 Weather Orb 天气球 为例,完整拆解一款 HarmonyOS 6.1 应用的实现过程:如何用 ArkTS / ArkUI 搭建沉浸式三栏界面,如何用 Canvas 2D 绘制会呼吸的天气球,如何在高频重绘中控制性能,如何把城市切换、预警、生活指数统一到一份状态中,以及如何把 AI 天气建议设计成可解释、可替换、可持续演进的体验。

项目代码位于:weather-orb-demo/。本文以实际工程实现为依据,文中“AI 天气管家”当前使用本地规则模拟交互与结果展示;最后会给出接入真实大模型服务时的结构化输出与安全改造方案。

图 1:Weather Orb 天气主页,Canvas 2D 天气球与底部三栏导航

图 1:天气页以“会呼吸的光球”作为视觉中心,实时天气、AI 建议与小时趋势围绕同一份城市状态展开。


在这里插入图片描述

一、为什么要重新做一个天气应用

天气是一个典型的高频信息场景:出门前看温度,午间看紫外线,下班前看降雨,运动前看空气质量。它的重要性毋庸置疑,但常见天气应用也有相同问题:信息层级很多,却很难在三秒内帮助用户完成决策。

用户真正关心的往往不是“相对湿度 68%”本身,而是:

  • 今天适合穿什么;
  • 午后是否该带伞;
  • 能不能去跑步;
  • 通勤会不会受影响;
  • 是否需要立即注意某项风险。

因此,Weather Orb 的目标并不是做一个覆盖所有气象能力的专业平台,而是完成一次产品和技术上的小实验:把天气从一张静态信息表,变成一份有呼吸感、能突出风险、能给出行动建议的轻量化体验。

这个项目最终形成三个层次:

  1. 天气页:用 Canvas 2D 绘制动态天气球,聚焦实时温度、核心指标、小时趋势;
  2. 预警页:把未来数小时的风险压缩成可扫读的时间线,并提供推送开关;
  3. 指数页:把紫外线、穿衣、运动、洗车等数据转为生活建议和 AI 今日安排。

它们不是三个孤立页面,而是共享同一个“当前城市”状态。用户切换深圳、杭州、成都后,球体颜色、温湿度、预警文案、舒适度和生活建议都应同步变化。

图 2:切换城市后,天气球的温度、天气状态与配色同步变化

图 2:城市切换不是只替换标题,而是驱动天气球、指标卡、预警和生活建议的一次完整状态刷新。


二、项目定位与核心特性

Weather Orb 基于 HarmonyOS 6.1、ArkTS 与 ArkUI 实现。为了让项目既适合模拟器验证,也方便后续迁移到真实天气 API 或大模型服务,工程没有把业务逻辑散落到 UI 中,而是先确定了几条约束。

2.1 三个页面,一个任务闭环

页面解决的问题核心信息
天气此刻天气怎样温度、天气球、湿度、风感、空气质量、小时趋势
预警接下来需要防什么风险等级、影响时间、行动提醒、预警订阅
指数今天适合做什么综合舒适度、紫外线、穿衣、运动、洗车、AI 安排

在 UI 上,它们通过固定底部导航组织:天气预警指数。导航固定在视觉底部,内容区域独立滚动,避免长内容遮挡切换入口。

2.2 四个突出技术特性

特性一:Canvas 2D 驱动的“会呼吸”天气球。

天气球不是一张静态图片,而是由多层渐变、径向辉光、轨道弧线、漂浮云朵和粒子共同组成。核心球体半径与光晕强度随正弦函数平滑波动,形成非常轻微但持续存在的呼吸节奏。

特性二:16ms 节拍下的轻量动画循环。

项目按约 16ms 的周期刷新画布,对应约 62.5 次每秒的理论更新节拍。重点不在追求“每帧画得更多”,而在于避免每帧发生状态重建、组件树重组或大量对象分配。

特性三:同一份城市状态驱动三类页面。

天气球、预警中心与生活指数不是各自复制一套城市数据。项目用 WeatherSession 收敛当前城市索引,并用 WeatherPreset 描述业务数据。这样页面间同步有明确边界,未来替换真实 API 也只需修改数据来源。

特性四:AI 建议从“展示层”起步,保留真实模型接入边界。

当前版本的 AI 天气管家通过本地规则模拟“生成中—生成结果”的交互,目的是先验证产品体验:AI 卡片应该出现在哪里、如何解释建议、城市变化后如何更新。生产版本再将规则层替换为服务端大模型接口,而不是把密钥直接写进应用。


三、总体架构:先让状态有归属,再让界面有表现

对于小型 Demo,很多人会把所有代码塞进一个页面。开始时很快,但只要出现“多个 Tab 共享城市”“页面之间同步状态”“后续接入网络数据”等需求,维护成本就会快速上升。

Weather Orb 的工程结构保持克制:

weather-orb-demo/
└── entry/src/main/
    ├── ets/
    │   ├── common/
    │   │   └── WeatherData.ets        # 城市天气模型与共享会话
    │   ├── entryability/
    │   │   └── EntryAbility.ets       # 窗口与沉浸式系统栏配置
    │   └── pages/
    │       └── IndexPage.ets          # 三栏导航、Canvas、预警、指数
    └── resources/
        └── base/profile/main_pages.json

在当前迭代中,三个 Tab 为了快速共享视觉和状态,被内嵌在 IndexPage 的不同 @Builder 中。工程仍保留可继续拆分的边界:后续只需把 AlertContentLifeContent 提取为独立组件或页面,并通过同一个 WeatherSession 获取状态,无须改变核心数据模型。

3.1 统一的数据契约

天气数据模型不是简单的“城市 + 温度”,还包含了 UI 渲染需要的色彩信息:

export interface WeatherPreset {
  city: string;
  condition: string;
  temperature: number;
  high: number;
  low: number;
  humidity: number;
  wind: string;
  air: string;
  tone: string;
  accent: string;
}

这里的 toneaccent 很关键。传统做法会让 UI 根据 condition === '晴朗' 写一串 if/else 来决定颜色;Weather Orb 把“主色”和“高光色”直接放入数据契约中。这样,天气数据不只是业务数值,也成为视觉主题的来源。

export const WEATHER_PRESETS: WeatherPreset[] = [
  { city: '深圳', condition: '晴朗', temperature: 29,
    high: 31, low: 26, humidity: 68, wind: '东南风 3级',
    air: '优 · 28', tone: '#FFB75E', accent: '#FFE0A3' },
  { city: '杭州', condition: '多云', temperature: 24,
    high: 27, low: 20, humidity: 74, wind: '东北风 2级',
    air: '优 · 35', tone: '#9AB7F6', accent: '#D7E5FF' },
  { city: '成都', condition: '小雨', temperature: 19,
    high: 21, low: 17, humidity: 88, wind: '北风 1级',
    air: '良 · 52', tone: '#6C8CEB', accent: '#B8CCFF' }
];

当真实天气 API 接入后,可以在服务层把原始天气编码映射为这份领域模型。页面不需要知道 API 字段名,也不用关心“晴天应该用什么颜色”。

3.2 共享状态:小而明确的 WeatherSession

项目当前只需要跨 Tab 共享“用户选中的城市”。与其过早引入复杂状态框架,不如使用一个边界明确的会话类:

export class WeatherSession {
  private static cityIndex: number = 0;

  static setCityIndex(index: number): void {
    WeatherSession.cityIndex = index;
  }

  static current(): WeatherPreset {
    return WEATHER_PRESETS[WeatherSession.cityIndex];
  }
}

UI 层的本地 @State weatherIndex 负责触发当前页面刷新;WeatherSession 则负责让其它内容区获取同一选择。点击城市后,处理顺序为:

更新 @State weatherIndex
        ↓
更新 WeatherSession.cityIndex
        ↓
清空上一城市的 AI 建议展示状态
        ↓
立即重绘 Canvas 天气球
        ↓
预警和指数 Tab 下次显示时读取同一座城市

这条链路很短,但避免了“首页显示深圳,预警页还是杭州”的典型状态割裂问题。


四、沉浸式窗口:全屏不等于粗暴隐藏系统栏

天气类应用需要足够的视觉沉浸感。深色背景如果被顶部白色状态栏或底部白色导航栏切断,会显得像页面没有铺满。

一开始很容易想到直接隐藏所有系统栏。但在模拟器和不同设备环境中,强制隐藏系统栏可能带来手势区域异常、窗口显示不稳定等问题。更稳妥的方案是:让内容延展到系统区域,同时保留系统栏交互,并把系统栏颜色设置为与应用背景一致。

const mainWindow = windowStage.getMainWindowSync();
mainWindow.setWindowLayoutFullScreen(true);
mainWindow.setWindowSystemBarEnable(['status', 'navigation']);
mainWindow.setWindowSystemBarProperties({
  statusBarColor: '#07131F',
  navigationBarColor: '#07131F',
  statusBarContentColor: '#FFFFFF',
  navigationBarContentColor: '#FFFFFF'
});

这段代码表达了三个层面的意图:

  • setWindowLayoutFullScreen(true):让应用布局获得沉浸式延展能力;
  • 保留 statusnavigation:不破坏用户熟悉的系统操作;
  • 深色系统栏 + 浅色图标:让系统区域与应用背景连续,同时保证状态信息可读。

这种做法比“完全隐藏”更适合普通工具类应用。它不是影院、游戏或全屏视频场景,不需要把系统交互完全拿掉;它需要的是视觉连贯和稳定可用。

图 3:沉浸式深色系统栏与应用内容保持视觉连续

图 3:全屏的正确目标不是夺走系统导航,而是让系统区域与应用视觉融为一体。


五、Canvas 2D:把一颗天气球拆成六层画面

Weather Orb 的视觉中心是一个 348 × 348 的 Canvas。它没有使用外部动画资源,而是用 Canvas API 逐帧绘制。这样做有三个好处:

  1. 天气状态可以直接驱动颜色、光晕与尺寸;
  2. 动画参数可精确控制,易于做性能优化;
  3. 项目不依赖大量图片素材,安装包更轻,风格也更统一。

5.1 第一层:深海背景渐变

背景不是纯色。纯色会让球体看起来像悬浮在一张平面卡片上;线性渐变可以建立远近层次。

const background = ctx.createLinearGradient(0, 0, ORB_SIZE, ORB_SIZE);
background.addColorStop(0, '#1B6684');
background.addColorStop(0.52, '#0F3B61');
background.addColorStop(1, '#091C34');
ctx.fillStyle = background;
ctx.fillRect(0, 0, ORB_SIZE, ORB_SIZE);

左上偏亮、右下偏暗的分布与后续光源方向一致。即使用户不注意渐变本身,也会自然感知到中心球体更有纵深。

5.2 第二层:大范围径向辉光

天气球外围的光不是阴影,而是另一层径向渐变:

const glow = ctx.createRadialGradient(176, 138, 12, 176, 138, 156 + breathing);
glow.addColorStop(0, data.accent);
glow.addColorStop(0.24, data.tone);
glow.addColorStop(0.68, 'rgba(65,151,194,0.15)');
glow.addColorStop(1, 'rgba(5,20,43,0)');
ctx.globalAlpha = 0.7;
ctx.fillStyle = glow;
ctx.fillRect(0, 0, ORB_SIZE, ORB_SIZE);

需要注意两个细节:

  • 光晕中心并不完全等于画布中心,而是略微偏上,给底部温度信息留出视觉空间;
  • 最后一档透明度为 0,避免产生一圈生硬的边缘。

5.3 第三层:核心球体与高光

核心球体同样使用径向渐变,而不是简单的单色圆。它的内核颜色更浅,外缘更饱和,从而模拟玻璃球内部被照亮的感觉:

const orb = ctx.createRadialGradient(145, 106, 8, 176, 151, orbRadius);
orb.addColorStop(0, '#FFF6C9');
orb.addColorStop(0.18, data.accent);
orb.addColorStop(0.6, data.tone);
orb.addColorStop(1, '#D66B35');
ctx.fillStyle = orb;
ctx.beginPath();
ctx.arc(176, 150, orbRadius, 0, Math.PI * 2);
ctx.fill();

shadowBlur 在这里的作用不是制造黑色投影,而是拓宽明亮边缘:

ctx.shadowColor = data.tone;
ctx.shadowBlur = 28 + breathing;

绘制核心球后一定要把 shadowBlur 重置为 0,否则后续的轨道线、云朵、粒子都会意外带光晕,既影响画面也影响性能。

5.4 第四层:旋转感轨道

三段不完整的圆弧比完整的同心圆更像一个被激活的能量场。圆弧的起止角度会随相位缓慢变化:

for (let index = 0; index < 3; index++) {
  const radius = 112 + index * 24 + breathing;
  ctx.beginPath();
  ctx.arc(176, 150, radius,
    0.15 + this.phase * 0.3,
    Math.PI * 1.18 + this.phase * 0.3);
  ctx.stroke();
}

这里并不需要真的模拟物理旋转。很小的角度变化已经足够让眼睛感知“它在流动”。这也是移动端动效设计的一个原则:能用暗示完成的动画,就不要用高成本的显式动画。

5.5 第五层:漂浮云朵

云朵由三个圆弧组合而成,避免引入图片资源:

function drawCloud(ctx: CanvasRenderingContext2D,
  x: number, y: number, scale: number, alpha: number): void {
  ctx.globalAlpha = alpha;
  ctx.fillStyle = '#FFFFFF';
  ctx.beginPath();
  ctx.arc(x, y, 18 * scale, 0, Math.PI * 2);
  ctx.arc(x + 24 * scale, y - 10 * scale, 25 * scale, 0, Math.PI * 2);
  ctx.arc(x + 51 * scale, y, 17 * scale, 0, Math.PI * 2);
  ctx.fill();
  ctx.globalAlpha = 1;
}

两个云层分别使用不同的正弦与余弦位移,速度和方向不同,便能避免“所有元素一起移动”的机械感。

5.6 第六层:稀疏粒子

粒子数量被控制在 18 个,坐标通过确定性公式计算:

for (let index = 0; index < 18; index++) {
  const x = (index * 71 + 27) % ORB_SIZE;
  const y = (index * 43 + 64) % 280;
  const radius = 1 + (index % 3) * 0.5;
  ctx.beginPath();
  ctx.arc(x, y, radius, 0, Math.PI * 2);
  ctx.fill();
}

为什么不用每帧 Math.random()?因为随机位置会使每一帧都完全不同,粒子会闪烁抖动;确定性位置让粒子像稳定存在于背景中,只通过透明度和整体光效参与氛围。


六、让球“呼吸”:用一个相位统一驱动所有微动效

“呼吸感”并不需要复杂的关键帧系统。项目维护一个逐帧递增的相位 phase

private phase: number = 0;
private animationTimer: number = -1;

private startAnimation(): void {
  if (this.animationTimer >= 0) return;
  this.animationTimer = setInterval(() => {
    this.phase += 0.025;
    this.renderOrb();
  }, FRAME_MS);
}

再通过正弦函数得到一个范围大约为 -5 到 +5 的呼吸量:

const breathing: number = Math.sin(this.phase) * 5;

它被同时用于:

  • 核心球体的半径;
  • 外围辉光半径;
  • 光晕模糊半径;
  • 轨道半径;
  • 云朵的横向偏移。

这种“一个相位,多处轻微响应”的设计非常重要。若每个元素用彼此无关的随机动画,画面会显得嘈杂;若所有元素共用同一个底层节奏,又允许不同公式带来微小差异,用户会感到它们属于同一个气象系统。

6.1 为什么是 16ms,而不是盲目追求更快

const FRAME_MS: number = 16;

16ms 大约对应 60fps 的更新节拍。这里需要理性看待“高帧率”:定时器触发 16ms 并不等于设备实际每秒都呈现 60 帧,真实表现仍受到设备刷新率、系统调度、GPU 合成、画布内容复杂度等因素影响。

但对于这个项目而言,16ms 是合理的上限目标:每帧绘制的图元数量少,没有大图片解码,没有复杂路径裁剪,也没有把状态更新写进动画循环。它的目的不是跑分,而是让球体缩放和轨道位移在视觉上连续。

6.2 三条性能底线

在 Canvas 2D 动画中,我始终遵守三条底线:

第一,不在每帧修改 @State

phase 是普通私有字段。若每 16ms 更新一次 @State,ArkUI 声明式组件树也会尝试响应式刷新,Canvas 的优势会被抵消。动画只调用 renderOrb(),不会让文本、列表、导航栏跟着重组。

第二,页面不可见时停止动画。

aboutToDisappear(): void {
  if (this.animationTimer >= 0) {
    clearInterval(this.animationTimer);
    this.animationTimer = -1;
  }
}

如果页面离开后仍持续绘制,不仅耗电,也可能因为多个动画循环叠加造成不可预期的性能问题。

第三,控制绘制层数与对象创建。

当前每帧只有一个渐变背景、两个径向渐变、三个弧线、两个云层和少量粒子。需要更复杂效果时,应优先考虑预计算静态层或降低更新频率,而不是无限增加每帧工作量。


七、ArkUI 三栏导航:一个根页面如何组织不互相干扰的内容区

应用根部采用 Column + Stack:Stack 负责把固定底部导航叠放在内容区之上,内容区则为每个 Tab 提供自己的 Scroll

build() {
  Column() {
    Stack({ alignContent: Alignment.Bottom }) {
      if (this.currentTab === 0) {
        this.WeatherContent();
      } else if (this.currentTab === 1) {
        this.AlertContent();
      } else {
        this.LifeContent();
      }
      this.BottomBar();
    }
    .width('100%')
    .layoutWeight(1);
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#07131F');
}

这种结构比“在每一页都重复写导航栏”更可靠:

  • 导航只维护一份;
  • Tab 切换只改变 currentTab
  • 每个内容区各自滚动;
  • 内容底部统一预留 98 像素,避免被 78 像素高的导航栏遮挡。
.padding({ left: 18, right: 18, bottom: 98 })

这个 bottom 很容易被忽略。固定底栏叠放时,如果内容没有额外底部空间,最后一张卡片会被导航遮住,用户也无法滚动到完整位置。视觉上看起来像“页面留白”,实际却是布局安全区没有留足。

图 4:底部固定三栏导航,天气、预警、指数在同一任务流内切换

图 4:预警 Tab 使用风险卡片与时间线突出“什么时候需要注意”,而不是堆叠所有气象字段。


八、预警中心:不要把天气数据原样搬过来

预警页最容易犯的错误,是把天气主页的温度、湿度、风速再复制一遍。这样用户切页后看到的是更多数字,而不是更强的决策信息。

Weather Orb 的预警页只回答三个问题:

  1. 是否需要留意? 用“需留意”与颜色标签快速建立风险感知;
  2. 风险是什么? 例如紫外线偏强、短时降雨、低能见度;
  3. 什么时候发生? 用时间线给出 13:0016:0018:30 等关键节点。
Text(this.weatherIndex === 2 ? '降雨持续,请留意路滑' : '午后紫外线偏强,请做好防晒')

Text(this.weatherIndex === 2
  ? '湿度较高,16:00 后短时降雨概率上升,建议携带雨具并预留通勤时间。'
  : '午后日照较强,户外活动建议避开 13:00 - 15:00,补充水分并使用防晒用品。')

这里的核心不是文案本身,而是把“数据”变成“风险—影响—行动”结构:

短时降雨概率上升
      ↓
路面湿滑、傍晚湿度升高
      ↓
携带雨具、预留通勤时间、驾车保持车距

未来接入真实预警数据时,可以让服务端或端侧规则把原始气象码映射为这一结构。不要把未经解释的告警编码直接展示给普通用户。

8.1 预警订阅的产品边界

页面底部提供了预警推送开关:

Toggle({ type: ToggleType.Switch, isOn: true })
  .selectedColor('#41B8D7')

当前 Demo 重点展示交互形态。生产版本应当补齐:

  • 用户是否授权通知;
  • 订阅的是当前城市还是关注城市;
  • 什么级别会触发推送;
  • 夜间、会议、勿扰状态如何处理;
  • 关闭后是否立即取消已排队任务。

天气预警是有价值的主动服务,但“有价值”不等于高频打扰。智能化服务的第一原则仍然是克制。


九、生活指数:从指标展示转向行动建议

指数页的定位不是“再做一张数据仪表盘”,而是让用户用更少阅读完成一天的安排。

页面先给出一个综合舒适度:

Text((this.weatherIndex === 2 ? 72 : this.weatherIndex === 1 ? 82 : 88).toString())
Text('综合舒适度')

再展示四项与日常直接相关的建议:紫外线、穿衣、运动、洗车。每张卡都包含“等级 + 一句话解释”,如“偏强 / 建议补涂防晒”“室内优先 / 建议室内拉伸”。

图 5:生活指数页将抽象天气数据转化为穿衣、运动、洗车等具体建议

图 5:指数页不追求指标越多越好,而是强调与用户行为直接相关的少量结论。

在产品设计上,综合分数有一个风险:它很容易显得“伪精确”。用户看到 88 分,可能追问为什么不是 87 或 89。因此,分数必须配合解释文案,明确它只是当前规则下的体验性归纳,不是气象专业评级。

当前规则为深圳、杭州、成都给出不同的模拟结果;真实版本可以引入温度、湿度、降水概率、AQI、紫外线等级、风力、用户偏好等参数,再让一个可解释规则或模型服务输出:

{
  "comfortScore": 82,
  "summary": "温度舒适,适合短途出行",
  "indices": [
    { "type": "sport", "level": "适宜", "reason": "傍晚降温且空气质量良好" }
  ],
  "suggestion": "17:30 后安排 30 分钟快走,注意补水"
}

这就是 AI 或规则服务应该输出的形态:结构化结论 + 可解释理由 + 明确行动建议。应用再把它渲染为稳定 UI,而不是直接把一段长文本塞进页面。


十、AI 天气管家:先验证体验,再接入真实大模型

天气主页中的 AI 天气管家有两个状态:默认引导态和生成结果态。

@State isThinking: boolean = false;
@State showInsight: boolean = false;
@State aiInsight: string = '午后紫外线偏强,建议在 15:00 前完成户外活动。';

点击“生成建议”后,当前版本用短暂延迟模拟模型推理过程:

private generateInsight(): void {
  if (this.isThinking) return;
  this.isThinking = true;
  setTimeout(() => {
    const data = this.currentWeather();
    this.aiInsight = `${data.city}当前${data.condition}${data.temperature}° 的体感相对舒适。` +
      `湿度 ${data.humidity}%${data.humidity > 80 ? ',雨具建议随身携带' : ',注意及时补水'}` +
      `${data.wind},很适合安排一段轻松的步行。`;
    this.showInsight = true;
    this.isThinking = false;
  }, 700);
}

这并不是为了把本地规则伪装成大模型。相反,它体现了一条实际开发策略:先把 AI 交互体验做对,再把 AI 能力接进去。

在真正接入模型前,可以先验证:

  • 用户是否愿意点击“生成建议”;
  • 生成状态要不要显示;
  • 建议应有多长;
  • 城市切换后旧建议是否要隐藏;
  • 建议放在首页是否会干扰核心天气阅读。

这些问题属于产品和交互层面,接不接模型都必须回答。

图 6:AI 天气管家在生成后给出结合温湿度、风力的可行动建议

图 6:AI 建议不是复述温度,而是把天气条件转化为用户可执行的行动。

10.1 接入大模型时,应该怎么设计请求

真实模型服务不应由客户端直接携带密钥调用。建议由 HarmonyOS App 请求自有服务端,服务端完成鉴权、限流、提示词注入和模型调用:

HarmonyOS App
    ↓ HTTPS
业务服务端 / 网关
    ├─ 验证用户身份与请求频率
    ├─ 获取真实天气数据
    ├─ 注入提示词与用户偏好
    ├─ 调用大模型
    └─ 校验并返回结构化 JSON

请求可以是:

{
  "city": "深圳",
  "weather": {
    "condition": "晴朗",
    "temperature": 29,
    "humidity": 68,
    "wind": "东南风 3级",
    "air": "优 · 28"
  },
  "userPreference": {
    "commute": true,
    "exercise": true
  }
}

而不要要求模型返回一段自由散文。更可靠的响应应为:

{
  "summary": "午后紫外线偏强,傍晚适合步行。",
  "actions": [
    "13:00 - 15:00 减少暴晒",
    "外出携带饮水",
    "17:30 后可安排轻运动"
  ],
  "riskLevel": "low",
  "disclaimer": "建议仅供日常出行参考"
}

端侧负责解析、展示和降级;服务端负责安全和模型调用;模型负责归纳与个性化表达。这三层不能混在一起。

10.2 AI 输出必须有边界

天气建议涉及出行甚至安全,必须避免把生成内容描述成官方预警或专业气象结论。特别是在强降雨、台风、雷暴等场景,应用应优先展示权威数据源和明确的风险等级;AI 只负责把复杂信息转化为更容易理解的提醒。

一个可行的优先级是:

官方预警 / 权威气象数据
        ↓
规则引擎确定风险等级
        ↓
AI 生成更易懂的解释与个性化建议
        ↓
端侧展示免责声明与数据更新时间

这样既发挥 AI 的语言理解能力,也不让它越过可靠性边界。


十一、从本地演示到真实服务:需要补齐哪些能力

Weather Orb 当前以预置城市数据为主,适合验证 UI、动效、状态联动和 AI 建议形态。如果要升级为可长期使用的应用,还需要补齐以下模块。

11.1 天气数据服务层

不要让页面直接调用 HTTP。建议新增 WeatherService.ets,负责:

  • 请求真实天气服务;
  • 将接口字段映射为 WeatherPreset 或更完整的领域对象;
  • 缓存最近成功结果;
  • 在离线、超时、接口异常时返回可解释错误。

页面只接收 WeatherViewState

interface WeatherViewState {
  loading: boolean;
  weather?: WeatherPreset;
  updatedAt: number;
  error?: string;
}

这样“加载中、成功、失败、缓存数据”就不会被隐藏在一堆 if/else 中。

11.2 城市与用户偏好持久化

当前 WeatherSession 是内存态。真实应用应把关注城市、默认城市、预警订阅、运动偏好等写入 Preferences。注意持久化的不是每一帧动画状态,而是用户有意配置的业务数据。

11.3 实时预警与通知

预警中心真正有价值的部分在后台触达。但通知不是越多越好:需要按预警等级、关注城市、用户静默时间、设备状态综合判断。HarmonyOS 的系统能力可以帮助服务在适当入口触达用户,但产品设计必须先定义“什么情况下值得打扰”。

11.4 多设备差异化呈现

本项目在手机模拟器上完成验证,模块配置也声明支持 phonetablet2in1。当页面扩展到平板和 PC 时,不应该简单放大 348px 的天气球,而应根据设备改变信息密度:

  • 手机:天气球 + 关键建议优先;
  • 平板:天气球与小时趋势并列;
  • 2in1 / PC:多城市对比、周趋势、预警时间线可同时展示。

“一次开发,多端体验”不是复制同一 UI,而是共享业务状态、根据屏幕能力重新组织信息。


十二、开发过程中的几个坑与修正

12.1 固定底栏遮挡滚动内容

最初把底部导航直接叠在 Scroll 上方时,最后一组内容会被遮挡。解决方案不是把导航移走,而是给所有内容区统一预留底部空间:

.padding({ left: 18, right: 18, bottom: 98 })

78px 是导航实际高度,额外的 20px 保留滚动余量。这个小细节决定了页面末尾是否“可完整阅读”。

12.2 系统栏全隐藏不一定更沉浸

强制隐藏系统栏在某些模拟器环境会造成不稳定显示。修正后采用“内容全屏延展 + 保留系统栏 + 深色同色背景”的策略。它既减少白边,也保留了用户熟悉的系统手势与状态信息。

12.3 动画定时器必须有生命周期

如果 Canvas 在页面每次出现时都启动一个新的 setInterval,而离开时没有清理,最终会出现多重循环同时绘制。项目在 aboutToDisappear 中主动清理,并在 startAnimation 中检测是否已经启动:

if (this.animationTimer >= 0) return;

这是所有手动动画循环都应具备的保护。

12.4 城市切换需要清理派生状态

用户从深圳切到成都时,之前生成的“深圳紫外线提醒”不能继续显示在成都天气下。因此切换城市时除了更新索引,还应把 showInsight 置为 false,要求用户基于当前数据重新生成建议。

这类派生状态清理很容易漏掉,但它决定了用户是否会信任应用。


十三、构建与验证

项目使用 DevEco Studio 与 HarmonyOS SDK 构建。命令行构建时,需要确保 SDK 与 JDK 环境指向 DevEco Studio 安装目录:

cd weather-orb-demo
unset NODE_OPTIONS
export DEVECO_SDK_HOME="/Applications/DevEco-Studio.app/Contents/sdk"
export JAVA_HOME="/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home"

/Applications/DevEco-Studio.app/Contents/tools/node/bin/node \
  /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js \
  --mode module -p module=entry@default -p product=default \
  -p requiredDeviceType=phone assembleHap

构建产物路径:

entry/build/default/outputs/default/entry-default-unsigned.hap

在模拟器验证时,建议不要只看“能否打开”,而是覆盖以下场景:

  • 启动后系统栏背景是否和应用连续;
  • 天气球是否持续平滑呼吸,离开页面后是否停止;
  • 切换三座城市后温度、配色、预警、生活建议是否共同变化;
  • 底部三栏导航是否始终可点击;
  • 内容滚到最底部时是否被导航遮挡;
  • AI 天气管家生成中是否禁止重复点击;
  • 成都小雨状态下是否正确给出雨具、室内运动等建议。

对动效项目来说,静态截图只能证明布局;手动体验才能发现掉帧、遮挡、触控层级和状态不同步等问题。


十四、结语:天气不应该只是数字,而应该帮助用户行动

Weather Orb 从一个“会呼吸的天气球”开始,但它想验证的并不只是 Canvas 2D 能画出什么效果。

真正重要的是这条链路:

气象数据
  ↓
可视化状态(颜色、光感、动效)
  ↓
风险判断(预警时间线)
  ↓
生活解释(穿衣、运动、洗车)
  ↓
个性化建议(AI 天气管家)
  ↓
用户行动

Canvas 让天气有了更直观的情绪和节奏;ArkUI 让三类信息能在同一任务流中切换;共享状态让城市变化不再割裂;AI 则提供了将数据翻译成行动建议的可能。

但 AI 并不是这个应用的中心。真正的中心始终是用户:用户是否能更快判断要不要带伞、能不能跑步、怎样安排通勤。只有当技术、动效、数据和建议都服务于这个问题时,一个天气应用才不只是“展示天气”,而是在帮助用户理解并安排一天。

后续,Weather Orb 还可以继续向真实天气服务、多城市订阅、卡片服务、跨设备展示与端云协同方向扩展。但无论功能如何增加,都会坚持同一个原则:信息应当有层次,动效应当有节制,AI 应当可解释,服务应当真正帮助用户行动。


在这里插入图片描述

附:项目亮点速览

  • HarmonyOS 6.1 + ArkTS / ArkUI;
  • Canvas 2D 绘制渐变、辉光、轨道、云层、粒子的天气球;
  • 16ms 节拍的轻量动画循环,避免逐帧触发声明式 UI 重建;
  • 深色沉浸式窗口与系统栏同色融合;
  • 天气 / 预警 / 指数固定三栏导航;
  • 城市状态统一驱动视觉主题、预警信息与生活建议;
  • AI 天气管家交互闭环,当前使用本地规则模拟,预留服务端大模型接入边界;
  • 面向真实服务演进的结构化数据、隐私安全与降级策略。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值