【鸿蒙开发实战】从零打造精美天气App - ArkUI高级UI与数据绑定详解
📖 前言
在移动互联网时代,天气应用几乎是人手必备的工具之一。今天,我将带大家使用鸿蒙ArkUI从零开发一款精美的天气应用。这个项目不仅涵盖了基础的UI布局和状态管理,还涉及到了渐变背景、条件渲染、自定义组件构建器、模拟数据设计等进阶内容。
先看最终效果:


一、项目概述
1.1 功能需求
这款天气App具备以下核心功能:
| 功能模块 | 详细说明 |
|---|---|
| 实时天气展示 | 温度、天气状况、最高/最低温度 |
| 空气质量指数 | AQI数值及等级描述 |
| 生活指数 | 紫外线强度、湿度、风速 |
| 逐小时预报 | 未来8小时天气趋势 |
| 7天预报 | 一周天气概况及温度曲线 |
| 城市切换 | 支持多个城市快速切换 |
| 动态主题 | 根据天气状况自动切换背景颜色 |
1.2 技术亮点
- 🎨 渐变背景 - 根据天气状况动态切换主题色
- 📊 温度条可视化 - 直观展示温度范围
- 🏗️ @Builder装饰器 - 封装可复用UI组件
- 📱 模态弹窗 - 优雅的城市选择交互
- 🔄 数据驱动UI - 状态管理自动更新视图
1.3 技术栈
| 技术 | 说明 |
|---|---|
| 开发框架 | ArkUI(声明式UI) |
| 开发语言 | ArkTS(TypeScript扩展) |
| API版本 | 23 |
| 开发工具 | DevEco Studio |
| 项目模型 | Stage模型 |
二、项目创建与环境配置
2.1 创建新项目
- 打开 DevEco Studio,选择 Create Project
- 选择 Empty Ability 模板
- 填写项目信息:
- Project name: WeatherApp
- Bundle name: com.example.weatherapp
- Save location: E:\HMproject\Project\WeatherApp
- Compile SDK: 选择最新的 HarmonyOS Next SDK
- Model: Stage
2.2 项目结构
WeatherApp/
├── AppScope/
│ ├── app.json5 # 应用全局配置
│ └── resources/ # 全局资源
├── entry/
│ ├── src/main/
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets # 应用入口
│ │ │ └── pages/
│ │ │ └── Index.ets # 主页面(核心)
│ │ └── resources/
│ │ ├── base/element/ # 字符串、颜色
│ │ ├── base/media/ # 图片资源
│ │ └── base/profile/ # 页面路由
│ ├── module.json5 # 模块配置
│ └── build-profile.json5 # 构建配置
└── oh_modules/ # 依赖模块
三、数据模型设计
3.1 天气信息数据结构
首先定义天气信息的数据接口:
interface WeatherInfo {
city: string // 城市名称
temp: number // 当前温度
cond: string // 天气状况(晴/多云/雨等)
humid: number // 湿度百分比
wind: string // 风速等级
uv: string // 紫外线强度
high: number // 当日最高温度
low: number // 当日最低温度
aqi: number // 空气质量指数
aqiDesc: string // AQI等级描述
}
3.2 小时预报数据结构
interface HourlyItem {
time: string // 时间点
temp: number // 温度
icon: string // 天气图标(emoji)
}
3.3 日预报数据结构
interface DailyItem {
day: string // 星期几
date: string // 日期
icon: string // 天气图标
high: number // 最高温度
low: number // 最低温度
desc: string // 天气描述
}
3.4 模拟天气数据
由于本示例不涉及真实API调用,我们准备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: '良' },
{ city: '广州市', temp: 31, cond: '雷阵雨', humid: 78, wind: '3级', uv: '强', high: 33, low: 25, aqi: 38, aqiDesc: '优' },
{ city: '深圳市', temp: 29, cond: '阵雨', humid: 72, wind: '3级', uv: '强', high: 31, low: 24, aqi: 42, aqiDesc: '优' },
{ city: '杭州市', temp: 27, cond: '晴', humid: 50, wind: '2级', uv: '中等', high: 29, low: 19, aqi: 68, aqiDesc: '良' },
{ city: '成都市', temp: 22, cond: '阴', humid: 70, wind: '2级', uv: '弱', high: 24, low: 17, aqi: 85, aqiDesc: '良' },
{ city: '武汉市', temp: 28, cond: '晴', humid: 55, wind: '3级', uv: '强', high: 31, low: 21, aqi: 78, aqiDesc: '良' },
{ city: '南京市', temp: 25, cond: '多云', humid: 58, wind: '3级', uv: '中等', high: 28, low: 19, aqi: 65, aqiDesc: '良' }
]
四、状态管理设计
4.1 状态变量定义
天气应用需要管理大量的状态数据:
@Entry
@Component
struct Index {
// 城市与基础天气信息
@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
@State currentAQIDesc: string = '良'
// UI交互状态
@State showCityPicker: boolean = false
// 预报数据
@State hourlyData: HourlyItem[] = this.generateHourlyData('晴', 26)
@State dailyData: DailyItem[] = this.generateDailyData('晴', 30, 18)
}
4.2 状态变量说明
| 状态变量 | 类型 | 作用 |
|---|---|---|
location |
string | 当前选中的城市 |
currentTemp |
number | 当前温度 |
currentCondition |
string | 天气状况 |
showCityPicker |
boolean | 控制城市选择弹窗显示 |
hourlyData |
HourlyItem[] | 逐小时预报数据 |
dailyData |
DailyItem[] | 7天预报数据 |
五、核心功能实现
5.1 动态渐变背景
根据天气状况动态切换背景颜色是本App的一大亮点:
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() {
// 头部天气展示区域
// ...
}
.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 generateHourlyData(cond: string, baseTemp: number): HourlyItem[] {
const times: string[] = ['现在', '15:00', '18:00', '21:00', '00:00', '03:00', '06:00', '09:00']
const icons: string[] = ['🌙', '🌙', '🌤️', '☀️', '☀️', '☀️', '🌤️', '🌙']
const result: HourlyItem[] = []
for (let i = 0; i < 8; i++) {
result.push({
time: times[i],
temp: baseTemp + Math.floor(Math.random() * 6) - 3, // 随机波动±3度
icon: icons[i]
})
}
result[0].temp = baseTemp // 当前时间保持实际温度
return result
}
5.4 7天预报数据生成
private generateDailyData(cond: string, high: number, low: number): DailyItem[] {
const days: string[] = ['今天', '明天', '周三', '周四', '周五', '周六', '周日']
const icons: string[] = ['☀️', '⛅', '🌧️', '☁️', '☀️', '☀️', '⛅']
const descs: string[] = ['晴', '多云', '小雨', '阴', '晴', '晴', '多云']
const result: DailyItem[] = []
for (let i = 0; i < 7; i++) {
result.push({
day: days[i],
date: '6月' + (i + 1) + '日',
icon: icons[i],
high: high + Math.floor(Math.random() * 6) - 3,
low: low + Math.floor(Math.random() * 4) - 2,
desc: descs[i]
})
}
return result
}
5.5 城市切换逻辑
private getWeatherByCity(city: string): WeatherInfo {
for (let i = 0; i < this.WEATHER_DATA.length; i++) {
if (this.WEATHER_DATA[i].city === city) {
return this.WEATHER_DATA[i]
}
}
return this.WEATHER_DATA[0] // 默认返回北京
}
private switchCity(city: string): void {
const data = this.getWeatherByCity(city)
// 更新所有状态
this.location = city
this.currentTemp = data.temp
this.currentCondition = data.cond
this.currentHumidity = data.humid
this.currentWind = data.wind
this.currentUV = data.uv
this.currentHigh = data.high
this.currentLow = data.low
this.currentAQI = data.aqi
this.currentAQIDesc = data.aqiDesc
// 重新生成预报数据
this.hourlyData = this.generateHourlyData(data.cond, data.temp)
this.dailyData = this.generateDailyData(data.cond, data.high, data.low)
// 关闭弹窗
this.showCityPicker = false
}
六、UI组件封装
6.1 @Builder装饰器详解
ArkUI提供了 @Builder 装饰器,用于封装可复用的UI组件。相比普通方法,@Builder 方法可以返回UI描述,实现组件级别的复用。
6.2 紧凑信息卡片
用于展示空气质量、紫外线、湿度、风速等指标:
@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,
this.currentAQI <= 50 ? '#34C759' : '#FF9F0A')
this.compactCard('☀️ 紫外线', this.currentUV, '', '#FF9F0A')
this.compactCard('💧 湿度', String(this.currentHumidity) + '%', '', '#5AC8FA')
this.compactCard('🌬️ 风速', this.currentWind, '', '#8E8E93')
}
6.3 逐小时预报项
@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.4 7天预报行
这是一个较为复杂的组件,包含温度条可视化:
@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)
.margin({ left: 4, right: 4 })
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 })
}
6.5 温度条辅助方法
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.6 城市选择按钮
@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. 顶部天气展示区(带渐变背景)
Column() { /* ... */ }
.linearGradient({ /* ... */ })
// 2. 信息卡片区域
Row() { /* compactCard */ }
// 3. 逐小时预报
Column() { /* ... */ }
// 4. 7天预报
Column() { /* ... */ })
// 5. 更新时间
Text('数据更新时间: 2026年6月1日 14:00')
}
}
// 城市选择弹窗(条件渲染)
if (this.showCityPicker) {
Column() { /* 弹窗内容 */ }
}
}
}
7.2 头部天气展示区
Column() {
// 城市选择按钮
Row() {
Blank()
Button(this.location + ' ▾')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.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)
.margin({ top: 4 })
// 天气状况
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 城市选择弹窗
使用 Stack 叠加布局实现模态弹窗效果:
if (this.showCityPicker) {
Column() {
Column() {
Text('选择城市')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.margin({ top: 20, bottom: 16 })
Row() {
this.cityButton(this.CITIES[0])
this.cityButton(this.CITIES[1])
this.cityButton(this.CITIES[2])
this.cityButton(this.CITIES[3])
}
.width('100%')
.padding({ left: 12, right: 12 })
.margin({ top: 4 })
Row() {
this.cityButton(this.CITIES[4])
this.cityButton(this.CITIES[5])
this.cityButton(this.CITIES[6])
this.cityButton(this.CITIES[7])
}
.width('100%')
.padding({ left: 12, right: 12 })
.margin({ top: 8 })
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 })
}
八、完整代码实现
8.1 主页面完整代码(Index.ets)
// 数据接口定义
interface WeatherInfo {
city: string
temp: number
cond: string
humid: number
wind: string
uv: string
high: number
low: number
aqi: number
aqiDesc: string
}
interface HourlyItem {
time: string
temp: number
icon: string
}
interface DailyItem {
day: string
date: string
icon: string
high: number
low: number
desc: string
}
@Entry
@Component
struct Index {
// 城市列表
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: '良' },
{ city: '广州市', temp: 31, cond: '雷阵雨', humid: 78, wind: '3级', uv: '强', high: 33, low: 25, aqi: 38, aqiDesc: '优' },
{ city: '深圳市', temp: 29, cond: '阵雨', humid: 72, wind: '3级', uv: '强', high: 31, low: 24, aqi: 42, aqiDesc: '优' },
{ city: '杭州市', temp: 27, cond: '晴', humid: 50, wind: '2级', uv: '中等', high: 29, low: 19, aqi: 68, aqiDesc: '良' },
{ city: '成都市', temp: 22, cond: '阴', humid: 70, wind: '2级', uv: '弱', high: 24, low: 17, aqi: 85, aqiDesc: '良' },
{ city: '武汉市', temp: 28, cond: '晴', humid: 55, wind: '3级', uv: '强', high: 31, low: 21, aqi: 78, aqiDesc: '良' },
{ city: '南京市', temp: 25, cond: '多云', humid: 58, wind: '3级', uv: '中等', high: 28, low: 19, aqi: 65, aqiDesc: '良' }
]
// 状态变量
@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
@State currentAQIDesc: string = '良'
@State showCityPicker: boolean = false
@State hourlyData: HourlyItem[] = this.generateHourlyData('晴', 26)
@State dailyData: DailyItem[] = this.generateDailyData('晴', 30, 18)
// 辅助方法...
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'
}
private getWeatherEmoji(cond: string): string {
if (cond === '晴') return '☀️'
if (cond === '多云') return '⛅'
if (cond === '阴') return '☁️'
if (cond === '小雨') return '🌦️'
if (cond === '阵雨') return '🌧️'
if (cond === '雷阵雨') return '⛈️'
return '🌤️'
}
private generateHourlyData(cond: string, baseTemp: number): HourlyItem[] {
const times: string[] = ['现在', '15:00', '18:00', '21:00', '00:00', '03:00', '06:00', '09:00']
const icons: string[] = ['🌙', '🌙', '🌤️', '☀️', '☀️', '☀️', '🌤️', '🌙']
const result: HourlyItem[] = []
for (let i = 0; i < 8; i++) {
result.push({ time: times[i], temp: baseTemp + Math.floor(Math.random() * 6) - 3, icon: icons[i] })
}
result[0].temp = baseTemp
return result
}
private generateDailyData(cond: string, high: number, low: number): DailyItem[] {
const days: string[] = ['今天', '明天', '周三', '周四', '周五', '周六', '周日']
const icons: string[] = ['☀️', '⛅', '🌧️', '☁️', '☀️', '☀️', '⛅']
const descs: string[] = ['晴', '多云', '小雨', '阴', '晴', '晴', '多云']
const result: DailyItem[] = []
for (let i = 0; i < 7; i++) {
result.push({ day: days[i], date: '6月' + (i + 1) + '日', icon: icons[i], high: high + Math.floor(Math.random() * 6) - 3, low: low + Math.floor(Math.random() * 4) - 2, desc: descs[i] })
}
return result
}
private getWeatherByCity(city: string): WeatherInfo {
for (let i = 0; i < this.WEATHER_DATA.length; i++) {
if (this.WEATHER_DATA[i].city === city) return this.WEATHER_DATA[i]
}
return this.WEATHER_DATA[0]
}
private switchCity(city: string): void {
const data = this.getWeatherByCity(city)
this.location = city
this.currentTemp = data.temp
this.currentCondition = data.cond
this.currentHumidity = data.humid
this.currentWind = data.wind
this.currentUV = data.uv
this.currentHigh = data.high
this.currentLow = data.low
this.currentAQI = data.aqi
this.currentAQIDesc = data.aqiDesc
this.hourlyData = this.generateHourlyData(data.cond, data.temp)
this.dailyData = this.generateDailyData(data.cond, data.high, data.low)
this.showCityPicker = false
}
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'
}
// @Builder组件
@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)
}
@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)
}
@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).margin({ left: 4, right: 4 })
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 })
}
@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) })
}
build() {
Stack() {
Scroll() {
Column() {
Column() {
Row() {
Blank()
Button(this.location + ' ▾').fontSize(18).fontWeight(FontWeight.Medium)
.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).margin({ top: 4 })
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]]
})
Row() {
this.compactCard('💨 空气质量', String(this.currentAQI), this.currentAQIDesc, this.currentAQI <= 50 ? '#34C759' : '#FF9F0A')
this.compactCard('☀️ 紫外线', this.currentUV, '', '#FF9F0A')
this.compactCard('💧 湿度', String(this.currentHumidity) + '%', '', '#5AC8FA')
this.compactCard('🌬️ 风速', this.currentWind, '', '#8E8E93')
}.width('100%').padding({ left: 12, right: 12 }).margin({ top: -20 })
Column() {
Row() { Text('⏰ 逐小时预报').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Color.White); Blank() }
.width('100%').padding({ left: 16, bottom: 10, top: 16 })
Row() {
this.hourlyItem(this.hourlyData[0]); this.hourlyItem(this.hourlyData[1])
this.hourlyItem(this.hourlyData[2]); this.hourlyItem(this.hourlyData[3])
this.hourlyItem(this.hourlyData[4]); this.hourlyItem(this.hourlyData[5])
this.hourlyItem(this.hourlyData[6]); this.hourlyItem(this.hourlyData[7])
}.width('100%').padding({ left: 8, right: 8 })
}.width('100%').margin({ top: 16 }).backgroundColor('#1C1C1E').borderRadius(16).padding({ bottom: 12 })
Column() {
Row() { Text('📅 7 天预报').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Color.White); Blank() }
.width('100%').padding({ left: 16, bottom: 8, top: 16 })
this.dailyRow(this.dailyData[0]); this.dailyRow(this.dailyData[1])
this.dailyRow(this.dailyData[2]); this.dailyRow(this.dailyData[3])
this.dailyRow(this.dailyData[4]); this.dailyRow(this.dailyData[5])
this.dailyRow(this.dailyData[6])
}.width('100%').margin({ top: 12 }).backgroundColor('#1C1C1E').borderRadius(16).padding({ bottom: 8 })
Text('数据更新时间: 2026年6月1日 14:00').fontSize(12).fontColor('#555555').margin({ top: 16, bottom: 24 })
}.width('100%').backgroundColor('#000000')
}.width('100%').height('100%')
if (this.showCityPicker) {
Column() {
Column() {
Text('选择城市').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White).margin({ top: 20, bottom: 16 })
Row() {
this.cityButton(this.CITIES[0]); this.cityButton(this.CITIES[1]); this.cityButton(this.CITIES[2]); this.cityButton(this.CITIES[3])
}.width('100%').padding({ left: 12, right: 12 }).margin({ top: 4 })
Row() {
this.cityButton(this.CITIES[4]); this.cityButton(this.CITIES[5]); this.cityButton(this.CITIES[6]); this.cityButton(this.CITIES[7])
}.width('100%').padding({ left: 12, right: 12 }).margin({ top: 8 })
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 })
}
}.width('100%').height('100%').backgroundColor('#000000')
}
}
九、运行效果展示
9.1 构建项目
在 DevEco Studio 中:
- 点击菜单 Build > Build Hap(s)/APP(s) > Build Hap(s)
- 等待构建完成
9.2 运行应用
点击运行按钮,选择模拟器或真机运行。
9.3 界面效果
晴天主题:

多云主题:

雨天主题:

城市选择弹窗:

十、核心知识点总结
10.1 ArkUI核心概念
| 概念 | 说明 |
|---|---|
@Entry |
页面入口装饰器 |
@Component |
组件装饰器 |
@State |
状态变量装饰器,驱动UI更新 |
@Builder |
组件构建器,封装可复用UI |
10.2 布局组件
| 组件 | 用途 |
|---|---|
Stack |
叠加布局,实现弹窗效果 |
Column |
垂直线性布局 |
Row |
水平线性布局 |
Scroll |
可滚动容器 |
Blank |
占位空白,自动填充 |
10.3 样式系统
// 链式调用
Text('示例')
.fontSize(16)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
// 渐变背景
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['#FF9F0A', 0], ['#FFD60A', 1]]
})
10.4 条件渲染
if (this.showCityPicker) {
// 显示弹窗
}
if (desc.length > 0) {
Text(desc) // 条件显示
}
十一、扩展思路
这个天气App还可以继续完善:
11.1 功能扩展
| 扩展项 | 实现方案 |
|---|---|
| 真实天气API | 接入和风天气、心知天气等API |
| 定位服务 | 使用 @ohos.geoLocationManager 获取当前位置 |
| 天气预警 | 显示气象预警信息 |
| 生活指数 | 穿衣、洗车、运动等指数 |
| 天气动画 | 下雨、下雪等动态效果 |
11.2 技术优化
| 优化项 | 说明 |
|---|---|
| 数据缓存 | 使用 @ohos.data.preferences 缓存天气数据 |
| 下拉刷新 | 添加刷新手势 |
| 骨架屏 | 加载时显示占位UI |
| 国际化 | 多语言支持 |
十二、常见问题
Q1: 渐变背景不显示?
确保 linearGradient 设置在 Column 或其他容器组件上,且颜色格式正确。
Q2: @Builder方法无法访问this?
@Builder 方法默认是静态的,需要定义为实例方法才能访问 this。
Q3: 模态弹窗如何阻止事件穿透?
在遮罩层添加 .onClick() 事件处理,但不在内部弹窗容器上添加。
Q4: 温度条如何实现动态宽度?
通过方法计算返回宽度字符串,在 @Builder 中动态绑定。
十三、总结
通过这个天气App的开发,我们学习了:
✅ ArkUI声明式UI开发模式
✅ 复杂数据结构设计
✅ 多状态变量管理
✅ @Builder组件封装与复用
✅ 渐变背景与动态主题
✅ Stack叠加布局与模态弹窗
✅ 条件渲染与动态UI
✅ 温度条可视化实现
虽然使用的是模拟数据,但整体架构已经具备了真实天气应用的核心要素。接入真实API后,这款App就可以投入实际使用了!
参考资料
如果这篇文章对你有帮助,欢迎点赞、收藏、评论!你的支持是我创作的动力! 🌤️
更多推荐


所有评论(0)