前言
学鸿蒙也有一段时间了,之前做了个掷骰子的小项目,这次想挑战点更有难度的。想了想,天气App挺合适的——UI复杂、数据多、交互丰富,正好练手。
这篇文章记录了整个开发过程,有思路、有代码、有踩坑,希望能帮到同样在学习鸿蒙的小伙伴。
为什么选天气App?
理由有几个:
- 功能全面- 数据展示、列表、弹窗,该有的都有
- UI有挑战- 渐变背景、卡片布局、温度条,够折腾
- 实用性强- 做完真能用(接个API就行)
- 能发挥创意- 主题切换、动画效果,随你玩
功能规划
开干之前先想清楚要做什么:
| 功能 | 说明 |
|---|---|
| 天气展示 | 温度、天气状况、最高最低温 |
| 生活指数 | 空气质量、紫外线、湿度、风速 |
| 逐小时预报 | 8小时天气趋势 |
| 7天预报 | 一周天气概况 |
| 城市切换 | 支持8个城市 |
| 动态主题 | 根据天气换背景颜色 |
项目创建
打开DevEco Studio,选Empty Ability模板。
填信息:
- 项目名:WeatherApp
- 包名:com.example.weatherapp
- 位置:E:\HMproject\Project\WeatherApp
点Finish,等项目初始化。
主要代码在entry/src/main/ets/pages/Index.ets。
数据设计
数据结构
先定义数据结构,这是整个App的基础:
// 天气信息interfaceWeatherInfo{city:string// 城市名temp:number// 当前温度cond:string// 天气状况(晴/多云/雨...)humid:number// 湿度wind:string// 风力uv:string// 紫外线high:number// 最高温low:number// 最低温aqi:number// 空气质量指数aqiDesc:string// 空气质量描述}// 小时预报interfaceHourlyItem{time:stringtemp:numbericon:string}// 日预报interfaceDailyItem{day:stringdate:stringicon:stringhigh:numberlow:numberdesc:string}模拟数据
这个项目先用模拟数据,以后再接真实API。我准备了8个城市的数据:
privatereadonlyWEATHER_DATA:WeatherInfo[]=[{city:'北京市',temp:26,cond:'晴',humid:45,wind:'3级',uv:'中等',high:30,low:18,aqi:72,aqiDesc:'良'},{city:'上海市',temp:24,cond:'多云',humid:62,wind:'4级',uv:'中等',high:27,low:20,aqi:55,aqiDesc:'良'},{city:'广州市',temp:31,cond:'雷阵雨',humid:78,wind:'3级',uv:'强',high:33,low:25,aqi:38,aqiDesc:'优'},// ... 其他城市]状态管理
ArkUI用@State管状态,状态变了UI自动更新。
我定义了一堆状态:
@Statelocation:string='北京市'// 当前城市@StatecurrentTemp:number=26// 当前温度@StatecurrentCondition:string='晴'// 天气状况@StatecurrentHumidity:number=45// 湿度@StatecurrentWind:string='3级'// 风力@StatecurrentUV:string='中等'// 紫外线@StatecurrentHigh:number=30// 最高温@StatecurrentLow:number=18// 最低温@StatecurrentAQI:number=72// AQI@StatecurrentAQIDesc:string='良'// AQI描述@StateshowCityPicker:boolean=false// 弹窗开关@StatehourlyData:HourlyItem[]=[]// 小时预报@StatedailyData:DailyItem[]=[]// 日预报看着多,其实每个都有用。
核心功能
动态主题切换
这是我做这个App最想实现的功能——根据天气自动换背景!
晴天用橙色,雨天用蓝色,阴天用灰色:
privategetBgGradient(cond:string):string{if(cond==='晴')return'#FF9F0A'// 橙色if(cond==='多云'||cond==='阴')return'#8E8E93'// 灰色if(cond.includes('雨'))return'#5AC8FA'// 蓝色return'#4A90D9'}privategetBgEnd(cond:string):string{if(cond==='晴')return'#FFD60A'if(cond==='多云'||cond==='阴')return'#636366'if(cond.includes('雨'))return'#007AFF'return'#87CEEB'}然后在Column上加渐变:
Column(){// 内容...}.linearGradient({direction:GradientDirection.Bottom,colors:[[this.getBgGradient(this.currentCondition),0],[this.getBgEnd(this.currentCondition),1]]})效果超棒!切到北京就是橙色的晴天,切到广州就是蓝色的雨天。
温度条可视化
7天预报里有温度条,我觉得这个挺酷的:
@BuilderdailyRow(item:DailyItem){Row(){// 左边:星期、图标、描述Text(item.day).width(48)Text(item.icon).width(30)Text(item.desc).width(36)Blank()// 最低温Text(String(item.low)+'°').width(32)// 温度条Column(){Column().width(this.tempBarWidth(item.low,item.high)).height(6).borderRadius(3).backgroundColor(this.tempBarColor(item.low,item.high))}.width(60).height(6).backgroundColor('#333333').borderRadius(3)// 最高温Text(String(item.high)+'°').width(32)}}温度条的宽度和颜色是动态计算的:
// 温差越大,条越宽privatetempBarWidth(low:number,high:number):string{returnMath.floor(((high-low)/20)*100+20)+'%'}// 温度越高,颜色越红privatetempBarColor(low:number,high:number):string{constavg=(low+high)/2if(avg>=28)return'#FF3B30'// 红色if(avg>=20)return'#FF9F0A'// 橙色return'#34C759'// 绿色}城市切换
点城市名,弹出选择器,选了就切换所有数据:
privateswitchCity(city:string):void{constdata=this.getWeatherByCity(city)// 更新所有状态this.location=citythis.currentTemp=data.tempthis.currentCondition=data.cond// ... 其他状态// 重新生成预报this.hourlyData=this.generateHourlyData(data.cond,data.temp)this.dailyData=this.generateDailyData(data.cond,data.high,data.low)// 关弹窗this.showCityPicker=false}组件封装
ArkUI有个@Builder装饰器,可以把UI封装成可复用的组件。
信息卡片
@BuildercompactCard(icon:string,value:string,desc:string,color:string){Column(){Text(icon).fontSize(20)Text(value).fontSize(16).fontWeight(FontWeight.Bold).fontColor(Color.White).margin({top:6})if(desc)Text(desc).fontSize(12).fontColor(color).margin({top:2})}.layoutWeight(1).alignItems(HorizontalAlign.Center)}用起来很方便:
Row(){this.compactCard('💨','72','良','#34C759')this.compactCard('☀️','中等','','#FF9F0A')this.compactCard('💧','45%','','#5AC8FA')this.compactCard('🌬️','3级','','#8E8E93')}城市按钮
@BuildercityButton(city:string){Button(city).height(44).borderRadius(22).backgroundColor(this.location===city?'#FF9F0A':'#2C2C2E').fontColor(this.location===city?Color.White:'#8E8E93').layoutWeight(1).onClick(()=>this.switchCity(city))}选中的城市高亮显示。
布局实现
整体结构
build(){Stack(){// 主内容Scroll(){Column(){// 1. 天气展示区(渐变背景)// 2. 信息卡片// 3. 小时预报// 4. 7天预报}}// 城市选择弹窗if(this.showCityPicker){// 弹窗内容}}}用Stack是为了叠加弹窗。
城市选择弹窗
if(this.showCityPicker){Column(){Column(){Text('选择城市').margin({top:20,bottom:16})Row(){this.cityButton('北京市')this.cityButton('上海市')this.cityButton('广州市')this.cityButton('深圳市')}Row(){this.cityButton('杭州市')this.cityButton('成都市')this.cityButton('武汉市')this.cityButton('南京市')}Button('取消').onClick(()=>this.showCityPicker=false)}.width('85%').backgroundColor('#1C1C1E').borderRadius(20)}.width('100%').height('100%').backgroundColor('#80000000')// 半透明遮罩.justifyContent(FlexAlign.Center)}踩坑记录
坑1:渐变背景不生效
一开始忘了加linearGradient,背景就是纯色。检查了好几遍才发现…
解决:在Column上正确添加linearGradient属性。
坑2:弹窗点内部也会关
遮罩层的onClick写在最外层,结果点弹窗内容也会触发。
解决:内部弹窗容器不要加onClick,只在外层遮罩加。
坑3:温度条宽度一样
一开始用固定宽度,看起来没差别。
解决:改成根据温差计算宽度。
坑4:切换城市背景没变
忘了currentCondition也要更新。
解决:在switchCity里更新所有相关状态。
运行效果
在DevEco Studio里运行,效果如下:
点击城市名:
不同天气的背景:
学到了啥
- 状态联动- 一个操作更新多个状态
- 渐变背景-
linearGradient的用法 - 条件渲染-
if控制弹窗显示 - 组件封装-
@Builder复用UI - 叠加布局-
Stack实现弹窗 - 动态样式- 方法返回颜色和宽度
后续计划
这个App还能继续完善:
- 接真实API- 和风天气、心知天气都行
- 加定位- 自动获取当前城市
- 天气动画- 下雨效果、飘雪效果
- 下拉刷新- 更新天气数据
- 多日预报- 15天天气
- 生活指数- 穿衣、洗车、运动建议
总结
这个天气App比之前的掷骰子复杂多了,但也更有成就感。动态主题切换是我最满意的功能,切城市的时候背景跟着变,感觉很高级。
ArkUI写起来确实舒服,声明式UI真是前端开发的大趋势。有React或Flutter经验的话上手很快。
有问题欢迎留言交流~