[HarmonyOS NEXT 实战案例四] 天气应用网格布局(下)

2025-06-06 22:45:13
104次阅读
0个评论

[HarmonyOS NEXT 实战案例四] 天气应用网格布局(下)

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

img_73aa8767.png

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组件实现复杂的网格布局,以及如何添加各种交互功能和高级特性,提升用户体验。这些技能可以应用到各种需要网格布局的场景中,如电商商品展示、照片墙、新闻列表等。

收藏00

登录 后评论。没有帐号? 注册 一个。