前言

最近在学鸿蒙,做了个小天气App练手。说实话,一开始觉得天气App不就展示个温度嘛,能有多难?结果做下来发现,要把UI做得好看、交互做得顺滑,还是有不少细节要处理的。

这篇文章把整个过程记录下来,希望能帮到同样在学习鸿蒙的小伙伴。


一、这是个啥项目?

就是一个天气应用,主要功能有:

  • 显示当前城市的天气(温度、天气状况)
  • 空气质量、紫外线、湿度、风速这些生活指数
  • 逐小时预报(8小时)
  • 7天天气预报
  • 支持切换城市(8个城市)
  • 根据天气自动换背景颜色(晴天橙色、雨天蓝色、阴天灰色)

二、创建项目

打开DevEco Studio,选 Empty Ability 模板。

填写信息:

  • 项目名:WeatherApp
  • 包名:com.example.weatherapp
  • 保存位置:你自己选

一路Next,项目就建好了。

项目目录不用太在意,主要代码都在 entry/src/main/ets/pages/Index.ets 这个文件里。


三、数据怎么设计?

3.1 天气信息

我用一个接口定义天气数据:

interface WeatherInfo {
  city: string      // 城市名
  temp: number      // 当前温度
  cond: string      // 天气状况
  humid: number     // 湿度
  wind: string      // 风力
  uv: string        // 紫外线
  high: number      // 最高温
  low: number       // 最低温
  aqi: number       // 空气质量指数
  aqiDesc: string   // 空气质量描述
}

3.2 小时预报

interface HourlyItem {
  time: string   // 时间
  temp: number   // 温度
  icon: string   // 图标
}

3.3 日预报

interface DailyItem {
  day: string    // 星期
  date: string   // 日期
  icon: string   // 图标
  high: number   // 最高温
  low: number    // 最低温
  desc: string   // 描述
}

3.4 模拟数据

这个项目用的是模拟数据,我准备了8个城市:

private readonly CITIES: string[] = [
  '北京市', '上海市', '广州市', '深圳市',
  '杭州市', '成都市', '武汉市', '南京市'
]

private readonly WEATHER_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: '良' },
  // ... 其他城市
]

四、状态怎么管理?

ArkUI用 @State 装饰器声明状态变量,变量变了界面自动更新。

我定义了这些状态:

@State location: string = '北京市'           // 当前城市
@State currentTemp: number = 26              // 当前温度
@State currentCondition: string = '晴'       // 天气状况
@State currentHumidity: number = 45          // 湿度
@State currentWind: string = '3级'           // 风力
@State currentUV: string = '中等'            // 紫外线
@State currentHigh: number = 30              // 最高温
@State currentLow: number = 18               // 最低温
@State currentAQI: number = 72               // AQI
@State currentAQIDesc: string = '良'         // AQI描述
@State showCityPicker: boolean = false       // 显示城市选择器?
@State hourlyData: HourlyItem[] = []         // 小时预报
@State dailyData: DailyItem[] = []           // 日预报

变量有点多,但都是必要的。


五、核心功能

5.1 根据天气换背景

这个功能我觉得挺酷的,天气不同,背景颜色也跟着变:

private getBgGradient(cond: string): string {
  if (cond === '晴') return '#FF9F0A'        // 橙色
  if (cond === '多云' || cond === '阴') return '#8E8E93'  // 灰色
  if (cond === '小雨' || cond === '阵雨' || cond === '雷阵雨') return '#5AC8FA'  // 蓝色
  return '#4A90D9'
}

private getBgEnd(cond: string): string {
  if (cond === '晴') return '#FFD60A'        // 亮黄
  if (cond === '多云' || cond === '阴') return '#636366'
  if (cond === '小雨' || cond === '阵雨' || cond === '雷阵雨') return '#007AFF'
  return '#87CEEB'
}

然后在Column上用渐变:

Column() {
  // 内容...
}
.linearGradient({
  direction: GradientDirection.Bottom,
  colors: [
    [this.getBgGradient(this.currentCondition), 0],
    [this.getBgEnd(this.currentCondition), 1]
  ]
})

5.2 天气图标

我用emoji代替图标,简单省事:

private getWeatherEmoji(cond: string): string {
  if (cond === '晴') return '☀️'
  if (cond === '多云') return '⛅'
  if (cond === '阴') return '☁️'
  if (cond === '小雨') return '🌦️'
  if (cond === '阵雨') return '🌧️'
  if (cond === '雷阵雨') return '⛈️'
  return '🌤️'
}

5.3 切换城市

点击城市名,弹出选择器,选了就切换:

private switchCity(city: string): void {
  const data = this.getWeatherByCity(city)
  this.location = city
  this.currentTemp = data.temp
  this.currentCondition = data.cond
  // ... 更新其他状态
  this.hourlyData = this.generateHourlyData(data.cond, data.temp)
  this.dailyData = this.generateDailyData(data.cond, data.high, data.low)
  this.showCityPicker = false
}

六、UI组件封装

ArkUI有个 @Builder 装饰器,可以封装可复用的UI组件。

6.1 信息卡片

显示空气质量、紫外线那些小卡片:

@Builder compactCard(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.length > 0) {
      Text(desc).fontSize(12).fontColor(color).margin({ top: 2 })
    }
  }
  .layoutWeight(1)
  .padding({ top: 10, bottom: 10 })
  .alignItems(HorizontalAlign.Center)
}

用的时候:

Row() {
  this.compactCard('💨 空气质量', String(this.currentAQI), this.currentAQIDesc, '#34C759')
  this.compactCard('☀️ 紫外线', this.currentUV, '', '#FF9F0A')
  this.compactCard('💧 湿度', String(this.currentHumidity) + '%', '', '#5AC8FA')
  this.compactCard('🌬️ 风速', this.currentWind, '', '#8E8E93')
}

6.2 小时预报项

@Builder hourlyItem(item: HourlyItem) {
  Column() {
    Text(item.time).fontSize(12).fontColor('#8E8E93')
    Text(item.icon).fontSize(22).margin({ top: 6 })
    Text(String(item.temp) + '°').fontSize(15).fontWeight(FontWeight.Medium).fontColor(Color.White).margin({ top: 6 })
  }
  .padding({ left: 10, right: 10 })
  .alignItems(HorizontalAlign.Center)
}

6.3 日预报行

这个带温度条,稍微复杂点:

@Builder dailyRow(item: DailyItem) {
  Row() {
    Text(item.day).fontSize(15).fontColor(Color.White).width(48)
    Text(item.icon).fontSize(18).width(30)
    Text(item.desc).fontSize(14).fontColor('#8E8E93').width(36)
    Blank()
    Text(String(item.low) + '°').fontSize(14).fontColor('#8E8E93').width(32).textAlign(TextAlign.End)
    
    // 温度条
    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) + '°').fontSize(14).fontColor(Color.White).width(32).textAlign(TextAlign.End)
  }
  .width('100%')
  .padding({ left: 16, right: 16, top: 6, bottom: 6 })
}

温度条宽度和颜色是动态计算的:

private tempBarWidth(low: number, high: number): string {
  return Math.floor(((high - low) / 20) * 100 + 20) + '%'
}

private tempBarColor(low: number, high: number): string {
  const avg = (low + high) / 2
  if (avg >= 28) return '#FF3B30'  // 红色
  if (avg >= 20) return '#FF9F0A'  // 橙色
  return '#34C759'                  // 绿色
}

6.4 城市选择按钮

@Builder cityButton(city: string) {
  Button(city)
    .fontSize(14)
    .fontWeight(FontWeight.Medium)
    .height(44)
    .borderRadius(22)
    .backgroundColor(this.location === city ? '#FF9F0A' : '#2C2C2E')
    .fontColor(this.location === city ? Color.White : '#8E8E93')
    .layoutWeight(1)
    .margin({ left: 4, right: 4 })
    .onClick(() => { this.switchCity(city) })
}

七、页面布局

7.1 整体结构

build() {
  Stack() {
    // 主内容
    Scroll() {
      Column() {
        // 1. 顶部天气区(带渐变背景)
        // 2. 信息卡片
        // 3. 小时预报
        // 4. 7天预报
        // 5. 更新时间
      }
    }
    
    // 城市选择弹窗
    if (this.showCityPicker) {
      Column() {
        // 弹窗内容
      }
    }
  }
}

Stack 是为了实现弹窗叠加效果。

7.2 顶部天气区

Column() {
  // 城市按钮
  Row() {
    Blank()
    Button(this.location + '  ▾')
      .fontSize(18)
      .fontColor(Color.White)
      .backgroundColor(Color.Transparent)
      .onClick(() => { this.showCityPicker = true })
    Blank()
  }
  .width('100%')
  .padding({ top: 30 })

  // 天气图标
  Text(this.getWeatherEmoji(this.currentCondition))
    .fontSize(64)
    .margin({ top: 8 })

  // 温度
  Text(String(this.currentTemp) + '°')
    .fontSize(80)
    .fontWeight(FontWeight.Bold)
    .fontColor(Color.White)

  // 天气状况
  Text(this.currentCondition)
    .fontSize(20)
    .fontColor('#FFFFFF')
    .opacity(0.9)

  // 最高最低
  Text('最高 ' + this.currentHigh + '°  最低 ' + this.currentLow + '°')
    .fontSize(15)
    .fontColor('#FFFFFF')
    .opacity(0.7)
    .margin({ top: 6 })
}
.width('100%')
.padding({ bottom: 28 })
.linearGradient({
  direction: GradientDirection.Bottom,
  colors: [
    [this.getBgGradient(this.currentCondition), 0],
    [this.getBgEnd(this.currentCondition), 1]
  ]
})

7.3 城市选择弹窗

if (this.showCityPicker) {
  Column() {
    Column() {
      Text('选择城市')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.White)
        .margin({ top: 20, bottom: 16 })
      
      Row() {
        this.cityButton('北京市')
        this.cityButton('上海市')
        this.cityButton('广州市')
        this.cityButton('深圳市')
      }
      
      Row() {
        this.cityButton('杭州市')
        this.cityButton('成都市')
        this.cityButton('武汉市')
        this.cityButton('南京市')
      }
      
      Button('取消')
        .fontSize(16)
        .fontColor('#FF9F0A')
        .backgroundColor('#2C2C2E')
        .width('90%')
        .height(44)
        .borderRadius(22)
        .margin({ top: 16, bottom: 20 })
        .onClick(() => { this.showCityPicker = false })
    }
    .width('85%')
    .backgroundColor('#1C1C1E')
    .borderRadius(20)
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#80000000')  // 半透明遮罩
  .justifyContent(FlexAlign.Center)
  .onClick(() => { this.showCityPicker = false })
}

八、运行效果

在DevEco Studio里点运行,效果如下:

在这里插入图片描述

切换城市:

在这里插入图片描述

不同天气的背景:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


九、踩坑记录

9.1 渐变背景不显示

一开始我忘了加 linearGradient,背景就是纯色。后来发现要在Column上加。

9.2 弹窗点内部也会关闭

一开始弹窗的遮罩层onClick写在最外层,结果点弹窗内部也会关闭。后来把内部弹窗容器的onClick留空,问题解决。

9.3 温度条宽度计算不对

一开始用固定宽度,后来改成根据温差计算,效果好多了。


十、学到了啥

  • 状态管理 - @State 驱动UI更新
  • 组件封装 - @Builder 复用UI
  • 渐变背景 - linearGradient
  • 条件渲染 - if 控制显示
  • 叠加布局 - Stack 实现弹窗
  • 动态样式 - 方法返回颜色、宽度

十一、后续可以做的

  • 接真实天气API
  • 加定位功能
  • 加天气预警
  • 加下拉刷新
  • 加天气动画(下雨效果之类的)

总结

这个天气App虽然用的是模拟数据,但UI和交互都做完了,整体效果还不错。ArkUI写起来确实挺舒服的,声明式UI果然香。

有问题欢迎评论区交流!


觉得有用的话,点个赞再走呗~ 👍

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐