[HarmonyOS NEXT 实战案例四] 天气应用网格布局(下)
[HarmonyOS NEXT 实战案例四] 天气应用网格布局(下)
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
1. 概述
在上一篇教程中,我们学习了如何使用GridRow和GridCol组件实现基本的天气应用网格布局。本篇教程将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的天气应用界面。
2. 响应式布局实现
2.1 断点响应设置
为了适应不同屏幕尺寸的设备,我们可以使用GridRow组件的breakpoints属性设置断点响应:
GridRow({
columns: { xs: 2, sm: 3, md: 4, lg: 6 },
gutter: { x: 16, y: 16 },
breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
})
这里我们设置了三个断点值:320vp、600vp和840vp,并根据窗口大小自动调整列数:
- 小于320vp:2列
- 320vp-600vp:3列
- 600vp-840vp:4列
- 大于840vp:6列
同时,我们还设置了水平和垂直方向都为16vp的间距。
2.2 不同断点下的布局效果
下表展示了不同断点下的天气详情网格布局效果:
断点 | 列数 | 适用设备 |
---|---|---|
<320vp | 2列 | 小屏手机 |
320vp-600vp | 3列 | 中屏手机 |
600vp-840vp | 4列 | 大屏手机/小屏平板 |
>840vp | 6列 | 平板/桌面设备 |
2.3 响应式布局实现代码
@Builder
private WeatherDetailsSection() {
Column() {
Text('详细信息')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Start)
GridRow({
columns: { xs: 2, sm: 3, md: 4, lg: 6 },
gutter: { x: 16, y: 16 },
breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
ForEach(this.weatherDetails, (item: WeatherDetail) => {
GridCol({ span: 1 }) {
// 天气详情项内容
}
})
}
.margin({ top: 12 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(16)
.margin({ top: 16, bottom: 16 })
}
3. 天气卡片优化
3.1 添加阴影效果
为了提升天气卡片的视觉层次感,我们可以添加阴影效果:
Column() {
// 当前天气信息内容
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({
radius: 8,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
我们为Column容器添加了阴影效果,设置了8vp的模糊半径、10%透明度的黑色阴影颜色、0vp的X轴偏移和2vp的Y轴偏移。
3.2 添加渐变背景
为了使当前天气信息卡片更加美观,我们可以添加渐变背景:
Column() {
// 当前天气信息内容
}
.width('100%')
.padding(16)
.borderRadius(16)
.shadow({
radius: 8,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
.linearGradient({
angle: 180,
colors: [['#4FC3F7', 0], ['#2196F3', 1]]
})
我们使用linearGradient属性设置了从浅蓝色到深蓝色的渐变背景,角度为180度(从上到下)。同时,我们需要调整文本颜色为白色,以便在深色背景上更好地显示:
Text(this.currentWeather.location)
.fontSize(16)
.fontColor(Color.White)
Text(this.currentWeather.updateTime)
.fontSize(12)
.fontColor('#E0E0E0')
.margin({ left: 8 })
Text(this.currentWeather.temperature.toString())
.fontSize(64)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('°C')
.fontSize(24)
.fontColor(Color.White)
.margin({ top: 12 })
Text(this.currentWeather.weatherType)
.fontSize(16)
.fontColor(Color.White)
.margin({ left: 4 })
3.3 添加天气图标动画
为了增强用户体验,我们可以为天气图标添加动画效果:
@State rotateAngle: number = 0;
@State scaleValue: number = 1;
// 在当前天气信息部分
Row() {
Image(this.currentWeather.weatherIcon)
.width(32)
.height(32)
.rotate({ z: 1, angle: this.rotateAngle })
.scale({ x: this.scaleValue, y: this.scaleValue })
.onAppear(() => {
this.startIconAnimation();
})
Text(this.currentWeather.weatherType)
.fontSize(16)
.fontColor(Color.White)
.margin({ left: 8 })
}
.margin({ top: 8 })
// 动画方法
private startIconAnimation(): void {
// 旋转动画
animateTo({
duration: 2000,
tempo: 0.5,
curve: Curve.Linear,
iterations: -1,
playMode: PlayMode.Alternate
}, () => {
this.rotateAngle = 10;
})
// 缩放动画
animateTo({
duration: 1500,
curve: Curve.Ease,
iterations: -1,
playMode: PlayMode.Alternate
}, () => {
this.scaleValue = 1.2;
})
}
我们为天气图标添加了旋转和缩放动画,使图标轻微摆动并放大缩小,增强了界面的生动性。
4. 交互功能实现
4.1 添加下拉刷新功能
为了提供更好的用户体验,我们可以添加下拉刷新功能:
@State refreshing: boolean = false;
build() {
Refresh({ refreshing: this.refreshing }) {
Scroll() {
Column() {
// 天气信息内容
}
.width('100%')
.padding(16)
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Vertical)
.width('100%')
.height('100%')
}
.onRefresh(() => {
this.refreshData();
})
.backgroundColor('#F5F5F5')
}
private refreshData(): void {
this.refreshing = true;
// 模拟网络请求
setTimeout(() => {
// 更新天气数据
this.refreshing = false;
}, 2000);
}
4.2 添加城市切换功能
为了支持多城市天气查看,我们可以添加城市切换功能:
// 城市接口
interface City {
name: string;
code: string;
}
@State currentCity: City = { name: '北京市', code: '101010100' };
private cities: City[] = [
{ name: '北京市', code: '101010100' },
{ name: '上海市', code: '101020100' },
{ name: '广州市', code: '101280101' },
{ name: '深圳市', code: '101280601' },
{ name: '杭州市', code: '101210101' }
];
@State showCitySelector: boolean = false;
// 在当前天气信息部分
Row() {
Row() {
Text(this.currentCity.name)
.fontSize(16)
.fontColor(Color.White)
Image($r('app.media.ic_arrow_down'))
.width(16)
.height(16)
.fillColor(Color.White)
.margin({ left: 4 })
}
.onClick(() => {
this.showCitySelector = true;
})
Text(this.currentWeather.updateTime)
.fontSize(12)
.fontColor('#E0E0E0')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
// 城市选择器弹窗
if (this.showCitySelector) {
Panel() {
Column() {
Text('选择城市')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 16, bottom: 16 })
List() {
ForEach(this.cities, (city: City) => {
ListItem() {
Row() {
Text(city.name)
.fontSize(16)
.fontColor('#333333')
if (this.currentCity.code === city.code) {
Image($r('app.media.ic_check'))
.width(16)
.height(16)
.fillColor('#2196F3')
}
}
.width('100%')
.padding(16)
.justifyContent(FlexAlign.SpaceBetween)
.onClick(() => {
this.changeCity(city);
this.showCitySelector = false;
})
}
.border({ width: { bottom: 1 }, color: '#F0F0F0', style: BorderStyle.Solid })
})
}
.width('100%')
Button('取消')
.width('90%')
.height(40)
.margin({ top: 16, bottom: 16 })
.onClick(() => {
this.showCitySelector = false;
})
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius({ topLeft: 16, topRight: 16 })
}
.mode(PanelMode.Half)
.dragBar(true)
.backgroundColor('#80000000')
}
// 切换城市方法
private changeCity(city: City): void {
this.currentCity = city;
this.refreshing = true;
// 模拟加载新城市的天气数据
setTimeout(() => {
// 更新天气数据
this.refreshing = false;
}, 1000);
}
4.3 添加天气详情点击事件
为天气详情项添加点击事件,实现查看详细信息的功能:
GridCol({ span: 1 }) {
Column() {
Row() {
Image(item.icon)
.width(16)
.height(16)
Text(item.title)
.fontSize(12)
.fontColor('#666666')
.margin({ left: 4 })
}
.width('100%')
Row() {
Text(item.value)
.fontSize(16)
.fontColor('#333333')
Text(item.unit)
.fontSize(12)
.fontColor('#666666')
.margin({ left: 2 })
}
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor('#F9F9F9')
.borderRadius(12)
.onClick(() => {
this.showDetailInfo(item);
})
}
// 显示详细信息方法
private showDetailInfo(detail: WeatherDetail): void {
AlertDialog.show({
title: detail.title,
message: `${detail.value}${detail.unit}\n\n${this.getDetailDescription(detail.title)}`,
autoCancel: true,
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
primaryButton: {
value: '确定',
action: () => {
console.info('点击确定按钮');
}
}
});
}
// 获取详细描述方法
private getDetailDescription(title: string): string {
switch (title) {
case '体感温度':
return '体感温度是人体感受到的温度,受到气温、湿度和风速的影响。';
case '湿度':
return '湿度是空气中水蒸气含量的度量,影响人体的舒适度和天气状况。';
case '气压':
return '气压是单位面积上的大气压力,气压变化可以预示天气变化。';
case '能见度':
return '能见度是指在日光下能见到远处黑色目标的最大水平距离。';
case '风速':
return '风速是指空气水平运动的速率,影响体感温度和天气变化。';
case '紫外线':
return '紫外线指数表示紫外线辐射的强度,影响皮肤健康和户外活动安排。';
default:
return '';
}
}
5. 高级特性实现
5.1 添加天气背景动效
为了增强用户体验,我们可以根据天气状况添加不同的背景动效:
@State particles: WeatherParticle[] = [];
// 在build方法中
Stack() {
// 天气背景动效
if (this.currentWeather.weatherType === '晴') {
ForEach(this.particles, (particle: WeatherParticle) => {
Circle({ width: particle.size })
.fill('#FFEB3B')
.opacity(particle.opacity)
.position({ x: particle.x, y: particle.y })
})
} else if (this.currentWeather.weatherType === '小雨' || this.currentWeather.weatherType === '中雨') {
ForEach(this.particles, (particle: WeatherParticle) => {
Line()
.width(1)
.height(particle.size)
.stroke('#FFFFFF')
.opacity(particle.opacity)
.position({ x: particle.x, y: particle.y })
})
} else if (this.currentWeather.weatherType === '多云') {
ForEach(this.particles, (particle: WeatherParticle) => {
Circle({ width: particle.size })
.fill('#FFFFFF')
.opacity(particle.opacity)
.position({ x: particle.x, y: particle.y })
})
}
// 主要内容
Refresh({ refreshing: this.refreshing }) {
Scroll() {
Column() {
// 天气信息内容
}
.width('100%')
.padding(16)
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Vertical)
.width('100%')
.height('100%')
}
.onRefresh(() => {
this.refreshData();
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
// 在aboutToAppear生命周期中初始化粒子
aboutToAppear() {
this.initParticles();
this.animateParticles();
}
// 初始化粒子方法
private initParticles(): void {
this.particles = [];
const count = 30;
for (let i = 0; i < count; i++) {
this.particles.push({
x: Math.random() * 360,
y: Math.random() * 640,
size: Math.random() * 10 + 2,
opacity: Math.random() * 0.5 + 0.3,
speed: Math.random() * 2 + 1
});
}
}
// 动画粒子方法
private animateParticles(): void {
const animation = () => {
this.particles = this.particles.map(p => {
let newY = p.y + p.speed;
if (newY > 640) {
newY = -10;
}
return { ...p, y: newY };
});
setTimeout(() => {
animation();
}, 50);
};
animation();
}
// 天气粒子接口
interface WeatherParticle {
x: number;
y: number;
size: number;
opacity: number;
speed: number;
}
5.2 添加空气质量指数卡片
为了提供更全面的天气信息,我们可以添加空气质量指数卡片:
// 空气质量接口
interface AirQuality {
aqi: number;
level: string;
advice: string;
pm25: number;
pm10: number;
no2: number;
so2: number;
co: number;
o3: number;
}
// 空气质量数据
private airQuality: AirQuality = {
aqi: 75,
level: '良',
advice: '空气质量可接受,但某些污染物可能对极少数异常敏感人群健康有较弱影响',
pm25: 35,
pm10: 68,
no2: 28,
so2: 9,
co: 0.8,
o3: 65
};
// 空气质量卡片构建器
@Builder
private AirQualitySection() {
Column() {
Row() {
Text('空气质量')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Start)
}
Row() {
Column() {
Text(this.airQuality.aqi.toString())
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor(this.getAqiColor(this.airQuality.aqi))
Text(this.airQuality.level)
.fontSize(14)
.fontColor(this.getAqiColor(this.airQuality.aqi))
.margin({ top: 4 })
}
.width('30%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
Column() {
Text(this.airQuality.advice)
.fontSize(14)
.fontColor('#333333')
.margin({ bottom: 8 })
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(2)
Row() {
this.AqiItem('PM2.5', this.airQuality.pm25.toString())
this.AqiItem('PM10', this.airQuality.pm10.toString())
this.AqiItem('NO₂', this.airQuality.no2.toString())
}
Row() {
this.AqiItem('SO₂', this.airQuality.so2.toString())
this.AqiItem('CO', this.airQuality.co.toString())
this.AqiItem('O₃', this.airQuality.o3.toString())
}
.margin({ top: 8 })
}
.width('70%')
.padding({ left: 16 })
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(16)
.margin({ top: 16 })
.shadow({
radius: 8,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
}
// 空气质量项构建器
@Builder
private AqiItem(title: string, value: string) {
Column() {
Text(title)
.fontSize(12)
.fontColor('#666666')
Text(value)
.fontSize(14)
.fontColor('#333333')
.margin({ top: 4 })
}
.width('33%')
}
// 获取AQI颜色方法
private getAqiColor(aqi: number): string {
if (aqi <= 50) {
return '#4CAF50'; // 优
} else if (aqi <= 100) {
return '#FFEB3B'; // 良
} else if (aqi <= 150) {
return '#FF9800'; // 轻度污染
} else if (aqi <= 200) {
return '#F44336'; // 中度污染
} else if (aqi <= 300) {
return '#9C27B0'; // 重度污染
} else {
return '#7E0023'; // 严重污染
}
}
5.3 添加生活指数卡片
为了提供更实用的天气信息,我们可以添加生活指数卡片:
// 生活指数接口
interface LifeIndex {
title: string;
level: string;
icon: ResourceStr;
advice: string;
}
// 生活指数数据
private lifeIndices: LifeIndex[] = [
{ title: '穿衣', level: '舒适', icon: $r("app.media.ic_clothing"), advice: '建议穿薄长袖衬衫、单裤等服装。' },
{ title: '洗车', level: '较适宜', icon: $r("app.media.ic_car_wash"), advice: '较适宜洗车,未来一天无雨,风力较小。' },
{ title: '感冒', level: '低发', icon: $r("app.media.ic_cold"), advice: '各项气象条件适宜,无明显降温过程,发生感冒机率较低。' },
{ title: '运动', level: '适宜', icon: $r("app.media.ic_sport"), advice: '天气较好,适宜户外运动,请注意防晒。' },
{ title: '紫外线', level: '中等', icon: $r("app.media.ic_uv"), advice: '紫外线强度中等,外出时建议涂抹SPF大于15、PA+的防晒霜。' },
{ title: '旅游', level: '适宜', icon: $r("app.media.ic_travel"), advice: '天气较好,适宜旅游,请注意防晒。' }
];
// 生活指数卡片构建器
@Builder
private LifeIndexSection() {
Column() {
Text('生活指数')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Start)
GridRow({
columns: { xs: 2, sm: 3, md: 3, lg: 3 },
gutter: { x: 16, y: 16 },
breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
ForEach(this.lifeIndices, (item: LifeIndex) => {
GridCol({ span: 1 }) {
Column() {
Row() {
Image(item.icon)
.width(24)
.height(24)
Column() {
Text(item.title)
.fontSize(14)
.fontColor('#333333')
Text(item.level)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding(12)
.backgroundColor('#F9F9F9')
.borderRadius(12)
.onClick(() => {
this.showLifeIndexDetail(item);
})
}
})
}
.margin({ top: 12 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(16)
.margin({ top: 16, bottom: 16 })
.shadow({
radius: 8,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
}
// 显示生活指数详情方法
private showLifeIndexDetail(index: LifeIndex): void {
AlertDialog.show({
title: `${index.title}指数 - ${index.level}`,
message: index.advice,
autoCancel: true,
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
primaryButton: {
value: '确定',
action: () => {
console.info('点击确定按钮');
}
}
});
}
6. 完整代码
以下是优化后的天气应用网格布局的完整代码(部分代码省略):
// 当前天气信息接口
interface CurrentWeather {
temperature: number; // 当前温度
weatherType: string; // 天气类型
weatherIcon: ResourceStr; // 天气图标
location: string; // 位置
updateTime: string; // 更新时间
}
// 天气详情信息接口
interface WeatherDetail {
title: string; // 标题
value: string; // 值
unit: string; // 单位
icon: ResourceStr; // 图标
}
// 每日天气预报接口
interface DailyForecast {
date: string; // 日期
day: string; // 星期几
weatherType: string; // 天气类型
weatherIcon: ResourceStr; // 天气图标
highTemp: number; // 最高温度
lowTemp: number; // 最低温度
}
// 每小时天气预报接口
interface HourlyForecast {
time: string; // 时间
weatherIcon: ResourceStr; // 天气图标
temperature: number; // 温度
}
// 城市接口
interface City {
name: string;
code: string;
}
// 空气质量接口
interface AirQuality {
aqi: number;
level: string;
advice: string;
pm25: number;
pm10: number;
no2: number;
so2: number;
co: number;
o3: number;
}
// 生活指数接口
interface LifeIndex {
title: string;
level: string;
icon: ResourceStr;
advice: string;
}
// 天气粒子接口
interface WeatherParticle {
x: number;
y: number;
size: number;
opacity: number;
speed: number;
}
@Component
export struct WeatherGrid {
@State refreshing: boolean = false;
@State rotateAngle: number = 0;
@State scaleValue: number = 1;
@State currentCity: City = { name: '北京市', code: '101010100' };
@State showCitySelector: boolean = false;
@State particles: WeatherParticle[] = [];
// 城市列表
private cities: City[] = [
{ name: '北京市', code: '101010100' },
{ name: '上海市', code: '101020100' },
{ name: '广州市', code: '101280101' },
{ name: '深圳市', code: '101280601' },
{ name: '杭州市', code: '101210101' }
];
// 当前天气信息
private currentWeather: CurrentWeather = {
temperature: 26,
weatherType: '晴',
weatherIcon: $r("app.media.sunny"),
location: '北京市海淀区',
updateTime: '10:30 更新'
};
// 天气详情信息
private weatherDetails: WeatherDetail[] = [
{ title: '体感温度', value: '28', unit: '°C', icon: $r("app.media.temperature") },
{ title: '湿度', value: '45', unit: '%', icon: $r("app.media.humidity") },
{ title: '气压', value: '1013', unit: 'hPa', icon: $r("app.media.pressure") },
{ title: '能见度', value: '25', unit: 'km', icon: $r("app.media.visibility") },
{ title: '风速', value: '3.5', unit: 'm/s', icon: $r("app.media.wind") },
{ title: '紫外线', value: '中等', unit: '', icon: $r("app.media.uv") }
];
// 每日天气预报
private dailyForecasts: DailyForecast[] = [
{ date: '6月1日', day: '今天', weatherType: '晴', weatherIcon: $r("app.media.sunny"), highTemp: 28, lowTemp: 18 },
{ date: '6月2日', day: '明天', weatherType: '多云', weatherIcon: $r("app.media.cloudy"), highTemp: 26, lowTemp: 17 },
{ date: '6月3日', day: '周五', weatherType: '小雨', weatherIcon: $r("app.media.rainy"), highTemp: 24, lowTemp: 16 },
{ date: '6月4日', day: '周六', weatherType: '阴', weatherIcon: $r("app.media.overcast"), highTemp: 25, lowTemp: 17 },
{ date: '6月5日', day: '周日', weatherType: '晴', weatherIcon: $r("app.media.sunny"), highTemp: 29, lowTemp: 19 }
];
// 每小时天气预报
private hourlyForecasts: HourlyForecast[] = [
{ time: '现在', weatherIcon: $r("app.media.sunny"), temperature: 26 },
{ time: '11:00', weatherIcon: $r("app.media.sunny"), temperature: 27 },
{ time: '12:00', weatherIcon: $r("app.media.sunny"), temperature: 28 },
{ time: '13:00', weatherIcon: $r("app.media.cloudy"), temperature: 28 },
{ time: '14:00', weatherIcon: $r("app.media.cloudy"), temperature: 27 },
{ time: '15:00', weatherIcon: $r("app.media.cloudy"), temperature: 26 },
{ time: '16:00', weatherIcon: $r("app.media.cloudy"), temperature: 25 },
{ time: '17:00', weatherIcon: $r("app.media.cloudy"), temperature: 24 }
];
// 空气质量数据
private airQuality: AirQuality = {
aqi: 75,
level: '良',
advice: '空气质量可接受,但某些污染物可能对极少数异常敏感人群健康有较弱影响',
pm25: 35,
pm10: 68,
no2: 28,
so2: 9,
co: 0.8,
o3: 65
};
// 生活指数数据
private lifeIndices: LifeIndex[] = [
{ title: '穿衣', level: '舒适', icon: $r("app.media.ic_clothing"), advice: '建议穿薄长袖衬衫、单裤等服装。' },
{ title: '洗车', level: '较适宜', icon: $r("app.media.ic_car_wash"), advice: '较适宜洗车,未来一天无雨,风力较小。' },
{ title: '感冒', level: '低发', icon: $r("app.media.ic_cold"), advice: '各项气象条件适宜,无明显降温过程,发生感冒机率较低。' },
{ title: '运动', level: '适宜', icon: $r("app.media.ic_sport"), advice: '天气较好,适宜户外运动,请注意防晒。' },
{ title: '紫外线', level: '中等', icon: $r("app.media.ic_uv"), advice: '紫外线强度中等,外出时建议涂抹SPF大于15、PA+的防晒霜。' },
{ title: '旅游', level: '适宜', icon: $r("app.media.ic_travel"), advice: '天气较好,适宜旅游,请注意防晒。' }
];
aboutToAppear() {
this.initParticles();
this.animateParticles();
}
build() {
Stack() {
// 天气背景动效
if (this.currentWeather.weatherType === '晴') {
ForEach(this.particles, (particle: WeatherParticle) => {
Circle({ width: particle.size })
.fill('#FFEB3B')
.opacity(particle.opacity)
.position({ x: particle.x, y: particle.y })
})
} else if (this.currentWeather.weatherType === '小雨' || this.currentWeather.weatherType === '中雨') {
ForEach(this.particles, (particle: WeatherParticle) => {
Line()
.width(1)
.height(particle.size)
.stroke('#FFFFFF')
.opacity(particle.opacity)
.position({ x: particle.x, y: particle.y })
})
} else if (this.currentWeather.weatherType === '多云') {
ForEach(this.particles, (particle: WeatherParticle) => {
Circle({ width: particle.size })
.fill('#FFFFFF')
.opacity(particle.opacity)
.position({ x: particle.x, y: particle.y })
})
}
// 主要内容
Refresh({ refreshing: this.refreshing }) {
Scroll() {
Column() {
// 当前天气信息
this.CurrentWeatherSection()
// 每小时天气预报
this.HourlyForecastSection()
// 空气质量
this.AirQualitySection()
// 每日天气预报
this.DailyForecastSection()
// 天气详情信息
this.WeatherDetailsSection()
// 生活指数
this.LifeIndexSection()
}
.width('100%')
.padding(16)
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Vertical)
.width('100%')
.height('100%')
}
.onRefresh(() => {
this.refreshData();
})
// 城市选择器弹窗
if (this.showCitySelector) {
Panel() {
Column() {
Text('选择城市')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 16, bottom: 16 })
List() {
ForEach(this.cities, (city: City) => {
ListItem() {
Row() {
Text(city.name)
.fontSize(16)
.fontColor('#333333')
if (this.currentCity.code === city.code) {
Image($r('app.media.ic_check'))
.width(16)
.height(16)
.fillColor('#2196F3')
}
}
.width('100%')
.padding(16)
.justifyContent(FlexAlign.SpaceBetween)
.onClick(() => {
this.changeCity(city);
this.showCitySelector = false;
})
}
.border({ width: { bottom: 1 }, color: '#F0F0F0', style: BorderStyle.Solid })
})
}
.width('100%')
Button('取消')
.width('90%')
.height(40)
.margin({ top: 16, bottom: 16 })
.onClick(() => {
this.showCitySelector = false;
})
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius({ topLeft: 16, topRight: 16 })
}
.mode(PanelMode.Half)
.dragBar(true)
.backgroundColor('#80000000')
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
private CurrentWeatherSection() {
Column() {
Row() {
Row() {
Text(this.currentCity.name)
.fontSize(16)
.fontColor(Color.White)
Image($r('app.media.ic_arrow_down'))
.width(16)
.height(16)
.fillColor(Color.White)
.margin({ left: 4 })
}
.onClick(() => {
this.showCitySelector = true;
})
Text(this.currentWeather.updateTime)
.fontSize(12)
.fontColor('#E0E0E0')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text(this.currentWeather.temperature.toString())
.fontSize(64)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('°C')
.fontSize(24)
.fontColor(Color.White)
.margin({ top: 12 })
}
.margin({ top: 16 })
Row() {
Image(this.currentWeather.weatherIcon)
.width(32)
.height(32)
.rotate({ z: 1, angle: this.rotateAngle })
.scale({ x: this.scaleValue, y: this.scaleValue })
.onAppear(() => {
this.startIconAnimation();
})
Text(this.currentWeather.weatherType)
.fontSize(16)
.fontColor(Color.White)
.margin({ left: 8 })
}
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.borderRadius(16)
.shadow({
radius: 8,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
.linearGradient({
angle: 180,
colors: [['#4FC3F7', 0], ['#2196F3', 1]]
})
}
// 其他构建器和方法省略...
private refreshData(): void {
this.refreshing = true;
// 模拟网络请求
setTimeout(() => {
// 更新天气数据
this.refreshing = false;
}, 2000);
}
private changeCity(city: City): void {
this.currentCity = city;
this.refreshing = true;
// 模拟加载新城市的天气数据
setTimeout(() => {
// 更新天气数据
this.refreshing = false;
}, 1000);
}
private startIconAnimation(): void {
// 旋转动画
animateTo({
duration: 2000,
tempo: 0.5,
curve: Curve.Linear,
iterations: -1,
playMode: PlayMode.Alternate
}, () => {
this.rotateAngle = 10;
})
// 缩放动画
animateTo({
duration: 1500,
curve: Curve.Ease,
iterations: -1,
playMode: PlayMode.Alternate
}, () => {
this.scaleValue = 1.2;
})
}
private showDetailInfo(detail: WeatherDetail): void {
AlertDialog.show({
title: detail.title,
message: `${detail.value}${detail.unit}\n\n${this.getDetailDescription(detail.title)}`,
autoCancel: true,
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
primaryButton: {
value: '确定',
action: () => {
console.info('点击确定按钮');
}
}
});
}
private getDetailDescription(title: string): string {
switch (title) {
case '体感温度':
return '体感温度是人体感受到的温度,受到气温、湿度和风速的影响。';
case '湿度':
return '湿度是空气中水蒸气含量的度量,影响人体的舒适度和天气状况。';
case '气压':
return '气压是单位面积上的大气压力,气压变化可以预示天气变化。';
case '能见度':
return '能见度是指在日光下能见到远处黑色目标的最大水平距离。';
case '风速':
return '风速是指空气水平运动的速率,影响体感温度和天气变化。';
case '紫外线':
return '紫外线指数表示紫外线辐射的强度,影响皮肤健康和户外活动安排。';
default:
return '';
}
}
private showLifeIndexDetail(index: LifeIndex): void {
AlertDialog.show({
title: `${index.title}指数 - ${index.level}`,
message: index.advice,
autoCancel: true,
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
primaryButton: {
value: '确定',
action: () => {
console.info('点击确定按钮');
}
}
});
}
private getAqiColor(aqi: number): string {
if (aqi <= 50) {
return '#4CAF50'; // 优
} else if (aqi <= 100) {
return '#FFEB3B'; // 良
} else if (aqi <= 150) {
return '#FF9800'; // 轻度污染
} else if (aqi <= 200) {
return '#F44336'; // 中度污染
} else if (aqi <= 300) {
return '#9C27B0'; // 重度污染
} else {
return '#7E0023'; // 严重污染
}
}
private initParticles(): void {
this.particles = [];
const count = 30;
for (let i = 0; i < count; i++) {
this.particles.push({
x: Math.random() * 360,
y: Math.random() * 640,
size: Math.random() * 10 + 2,
opacity: Math.random() * 0.5 + 0.3,
speed: Math.random() * 2 + 1
});
}
}
private animateParticles(): void {
const animation = () => {
this.particles = this.particles.map(p => {
let newY = p.y + p.speed;
if (newY > 640) {
newY = -10;
}
return { ...p, y: newY };
});
setTimeout(() => {
animation();
}, 50);
};
animation();
}
}
7. 总结
本教程详细讲解了如何优化天气应用的网格布局,添加交互功能,以及实现更多高级特性。通过使用GridRow和GridCol组件的高级特性,我们实现了响应式布局,使应用能够适应不同屏幕尺寸的设备。同时,我们还添加了卡片优化、交互功能、天气背景动效、空气质量指数卡片和生活指数卡片等功能,打造了一个功能完善的天气应用界面。
通过本教程,你应该已经掌握了如何使用HarmonyOS NEXT的GridRow和GridCol组件实现复杂的网格布局,以及如何添加各种交互功能和高级特性,提升用户体验。这些技能可以应用到各种需要网格布局的场景中,如电商商品展示、照片墙、新闻列表等。
- 0回答
- 3粉丝
- 0关注
- [HarmonyOS NEXT 实战案例四] 天气应用网格布局(上)
- [HarmonyOS NEXT 实战案例五] 社交应用照片墙网格布局(下)
- [HarmonyOS NEXT 实战案例六] 餐饮菜单网格布局(下)
- [HarmonyOS NEXT 实战案例七] 健身课程网格布局(下)
- [HarmonyOS NEXT 实战案例九] 旅游景点网格布局(下)
- [HarmonyOS NEXT 实战案例一] 电商首页商品网格布局(下)
- [HarmonyOS NEXT 实战案例八] 电影票务网格布局(下)
- [HarmonyOS NEXT 实战案例五] 社交应用照片墙网格布局(上)
- [HarmonyOS NEXT 实战案例六] 餐饮菜单网格布局(上)
- [HarmonyOS NEXT 实战案例七] 健身课程网格布局(上)
- [HarmonyOS NEXT 实战案例九] 旅游景点网格布局(上)
- [HarmonyOS NEXT 实战案例一] 电商首页商品网格布局(上)
- [HarmonyOS NEXT 实战案例八] 电影票务网格布局(上)
- [HarmonyOS NEXT 实战案例三] 音乐专辑网格展示(下)
- [HarmonyOS NEXT 实战案例二] 新闻资讯网格列表(下)