【HarmonyOS】天气卡片
一张天气卡片的诞生我在鸿蒙 ArkUI 里揉捏光影与渐变我从未想过一个看起来很简单的天气卡片能让我在一个深夜里和 ArkTS 的类型系统反复拉锯。但当那道蓝紫渐变终于在模拟器屏幕上亮起来的时候我承认——值了。缘起一个简单的需求故事的开头很普通。我想在鸿蒙原生应用里做一张天气卡片需求描述只有一句话显示城市名、温度、天气状况再加上湿度和风速。听起来像是前端入门级别的 UI 卡片。如果你来自 Web 开发世界你大概会想一个 flex 容器几个 span 标签半小时搞定。但鸿蒙的 ArkUI 不是 Web。它有自己的语法树、自己的类型系统、自己的资源管理哲学。这些差异看起来细微实则深刻——就像你以为自己在学一种新的方言结果发现它是一门完全不同的语言只是碰巧借了几个词。这篇博文就是我和这张天气卡片之间从看起来简单到真的跑起来了的完整历程。整体架构两层 Column 的哲学先说布局。鸿蒙 ArkUI 的组件树没有 HTML 那种 div 嵌套 div 的随意感——每一个容器都有它的语义和约束。我的天气卡片用了两层ColumnColumn全屏画布负责居中和底色 └─ Column卡片本体负责渐变、圆角、阴影、内容排列 ├─ 天气图标 ☀️ ├─ 温度 28°C ├─ 天气描述 Sunny ├─ 城市名 Beijing ├─ 装饰分隔线 └─ 详情行 Row ├─ 湿度栏 Column ├─ 竖向分割线 Divider └─ 风速栏 Column为什么是两层因为外层Column做的事和内层完全不同外层占满全屏width(100%).height(100%)设置页面底色#F0F4F8把卡片垂直水平居中justifyContent(FlexAlign.Center)内层卡片自身宽度85%承载渐变背景、圆角、投影和所有内容组件这种页面壳 卡片体的分离在 Web 里你可能用 body div 随手实现但在 ArkUI 里它是一种推荐的架构范式——让每个容器只做一件事。资源体系当你不再硬编码如果你是从 Web 开发转过来的把颜色值和字号写在资源文件里这件事可能让你觉得多余——CSS 变量不也能做吗但鸿蒙的资源体系远不止变量替换。它是一套多设备、多语言、多主题的适配系统。颜色资源color.json我把所有颜色定义在entry/src/main/resources/base/element/color.json里{color:[{name:weather_card_bg_start,value:#4A90D9},{name:weather_card_bg_end,value:#7EC8E3},{name:weather_text_primary,value:#FFFFFF},{name:weather_text_secondary,value:#E0F0FF},{name:weather_detail_bg,value:#33FFFFFF}]}五个颜色各有归属#4A90D9是渐变起始色一种偏冷的蓝色暗示晴朗天空#7EC8E3是渐变终止色更浅更亮过渡到天空的边际#FFFFFF白色用于温度数字和城市名——这是卡片上的主角必须最醒目#E0F0FF是一种极淡的蓝白色用于Sunny这种描述性文字——存在但不抢戏#33FFFFFF是带 20% 透明度的白色用于分隔线和详情区域的装饰线——若隐若现当你把配色逻辑从这个组件用什么颜色升级到整个色板讲什么故事设计感就会自然浮现。尺寸资源float.json{float:[{name:weather_temp_font,value:64fp},{name:weather_icon_font,value:48fp},{name:weather_city_font,value:24fp},{name:weather_desc_font,value:18fp},{name:weather_detail_font,value:16fp},{name:weather_card_radius,value:32vp},{name:weather_card_padding,value:24vp}]}这里有一个很多人忽略的细节fp和vp的区别。fpfont pixel跟随系统字体缩放设置。如果用户把系统字体调大64fp的温度数字也会跟着变大——这对无障碍适配至关重要vpvirtual pixel跟随屏幕密度。它不随字体设置变化适合间距、圆角这类纯几何属性所以温度用fp圆角用vp这是有道理的。文案资源string.json{string:[{name:weather_humidity,value:Humidity},{name:weather_wind,value:Wind}]}静态文案统一走$r()引用。这样做最直接的好处是——未来加中文支持只需要创建zh-CN/element/string.json系统自动匹配代码零改动。渐变从色块到天空天气卡片最核心的视觉语言就是渐变。没有渐变它就是一张白色方块上有几个字有了渐变它就是一扇通往晴空的窗。在 ArkUI 中渐变通过.linearGradient()修饰器实现.linearGradient({direction:GradientDirection.RightBottom,colors:[[$r(app.color.weather_card_bg_start),0.0],[$r(app.color.weather_card_bg_end),1.0]]})这里的direction: GradientDirection.RightBottom表示渐变从左上角流向右下角。颜色从深蓝#4A90D9过渡到浅蓝#7EC8E30.0 到 1.0 是渐变的起止位置。我最初写成了GradientDirection.BottomRight——这看起来完全合理对吧毕竟我们习惯说从上到下从左到右。但 ArkUI 的命名规则是先水平后垂直RightBottom才是合法值。BottomRight会直接报编译错误。这是一个小坑但它暴露了一个深层差异ArkUI 的枚举命名有它自己的逻辑体系你不能用 Web CSS 的to bottom right语法直觉来套。阴影让卡片浮起来平面设计里有一个经典法则如果两个元素在同一层用颜色区分如果它们在不同层用阴影区分。我们的卡片浮在#F0F4F8的背景之上所以它需要有阴影.shadow({radius:24,color:0x334A90D9,offsetX:0,offsetY:8})四个参数拆解参数值含义radius24模糊半径越大越柔和color0x334A90D9阴影颜色33是约 20% 透明度的前缀4A90D9是蓝色offsetX0水平偏移0 表示阴影不左右偏offsetY8向下偏移 8vp模拟光从上方来的自然光效果阴影颜色选了蓝色而非纯黑这是一个常用的设计技巧——蓝色阴影比黑色阴影更轻盈更空气感。纯黑阴影会让卡片看起来像剪贴画而蓝色阴影让它看起来像悬浮在空气中的实物。详情栏layoutWeight 的等宽魔法卡片底部的湿度和风速两栏我用了Row 两个ColumnlayoutWeight(1)实现Row(){Column(){Text().fontSize(20)Text($r(app.string.weather_humidity)).fontSize($r(app.float.weather_detail_font))Text(this.humidity).fontSize($r(app.float.weather_detail_font))}.layoutWeight(1).alignItems(HorizontalAlign.Center)Divider().height(80%).width(1).color($r(app.color.weather_detail_bg))Column(){Text(️).fontSize(20)Text($r(app.string.weather_wind)).fontSize($r(app.float.weather_detail_font))Text(this.windSpeed).fontSize($r(app.float.weather_detail_font))}.layoutWeight(1).alignItems(HorizontalAlign.Center)}layoutWeight(1)的效果类似于 CSS 的flex: 1——每个子元素在剩余空间里平均分配。两个 Column 各拿一半自然等宽。竖向Divider是一个小细节height(80%)让它只占行高的 80%两端留出呼吸空间视觉上比通栏分割线精致得多。State 与 $r()类型系统的冷峻温柔这是我在这个项目里花时间最多的地方。最初我想当然地写了Statetemperature:string$r(app.string.weather_temp);报错。$r()返回的是Resource类型而State temperature声明的是string。ArkTS 是静态类型语言不允许Resource赋值给string。这意味着什么你需要把静态展示和动态数据两种场景分开处理。静态展示Text($r(app.string.weather_humidity))——标签文案永远不变直接用资源引用动态数据State temperature: string 28°C——这个值未来会从 API 更新所以用State存字面量这个区分初看繁琐但逻辑上是自洽的$r()是编译时的资源绑定State是运行时的状态管理。它们服务于不同的生命周期不应该混为一谈。装饰线60% 宽度的克制卡片中间有一条水平分隔线把主天气信息和详情栏分开Divider().width(60%).color($r(app.color.weather_detail_bg)).height(1).margin({top:20,bottom:20})宽度只取 60%不是 100%。为什么因为 100% 宽度的分割线会给人一种斩断的感觉——它说上面和下面是两个世界。而 60% 宽度的线更像一个停顿——它说到这里告一段落下面是补充信息。同样的颜色#33FFFFFF20% 透明白也服务于这个目的足够看到但不至于刺眼。模拟器初见当我第一次在模拟器上看到这张卡片时说实话有一点意外——比我想象的好看。蓝色的渐变从左上到右下白色文字在蓝色底上很清晰投影让卡片确实浮在背景之上。底部的湿度和风速两栏对称分布中间一条细细的竖线一切都恰到好处。踩坑清单给后来者的路标坑症状解法$r()赋值给State string编译报类型错误State存字面量$r()只写在组件属性里GradientDirection.BottomRight编译报枚举不存在改为GradientDirection.RightBottom先水平后垂直阴影颜色用纯黑卡片看起来假改用主题色的低透明度版本竖线 Divider 默认横向看不到竖线手动设height(80%).width(1)后记一张天气卡片代码量不大但它让我理解了 ArkUI 的几个核心哲学资源与逻辑分离颜色、尺寸、文案都不属于代码它们属于资源体系类型即约束ArkTS 的静态类型不是负担是设计——它逼迫你在写代码之前想清楚什么是静态的、什么是动态的装饰的克制60% 宽度的分隔线、20% 透明度的颜色、80% 高度的竖线——好的 UI 不在于多加什么而在于少加多少这张卡片只是一个起点。未来加上真实的天气 API、城市切换、动态图标和入场动画它会从一张静态卡片变成一个真正有用的天气应用。