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

简介: 在上一篇教程中,我们学习了如何使用GridRow和GridCol组件实现基本的天气应用网格布局。本篇教程将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的天气应用界面。

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

相关文章
|
15天前
|
开发者 UED
HarmonyOS Next快速入门:通用属性
本教程以《HarmonyOS Next快速入门》为基础,涵盖应用开发核心技能。通过代码实例讲解尺寸、位置、布局约束、Flex布局、边框、背景及图像效果等属性设置方法。如`.width()`调整宽度,`.align()`设定对齐方式,`.border()`配置边框样式,以及模糊、阴影等视觉效果的实现。结合实际案例,帮助开发者掌握HarmonyOS组件属性的灵活运用,提升开发效率与用户体验。适合初学者及进阶开发者学习。
56 0
|
15天前
|
开发者
HarmonyOS Next快速入门:通用事件
本教程聚焦HarmonyOS应用开发,涵盖事件处理的核心内容。包括事件分发、触屏事件、键鼠事件、焦点事件及拖拽事件等。通过代码实例讲解点击事件、触控事件(Down/Move/Up)、获焦与失焦事件的处理逻辑,以及气泡弹窗的应用。适合开发者快速掌握HarmonyOS Next中通用事件的使用方法,提升应用交互体验。
56 0
|
15天前
|
开发者 容器
HarmonyOS Next快速入门:Button组件
本教程摘自《HarmonyOS Next快速入门》,聚焦HarmonyOS应用开发中的Button组件。Button支持胶囊、圆形和普通三种类型,可通过子组件实现复杂功能,如嵌入图片或文字。支持自定义样式(边框弧度、文本样式、背景色等)及点击事件处理。示例代码展示了不同类型按钮的创建与交互逻辑,助开发者快速上手。适合HarmonyOS初学者及对UI组件感兴趣的开发者学习。
57 0
|
15天前
|
开发者
HarmonyOS Next快速入门:Image组件
本教程摘自《HarmonyOS Next快速入门》,专注于HarmonyOS应用开发中的Image组件使用。Image组件支持多种图片格式(如png、jpg、svg等),可渲染本地资源、网络图片、媒体库文件及PixelMap像素图。通过设置`objectFit`属性,实现不同缩放类型;利用`fillColor`属性调整矢量图颜色。示例代码涵盖本地、网络及资源图片的加载与样式设置,同时需在`module.json5`中声明网络权限以加载外部资源。适合开发者快速掌握HarmonyOS图像展示功能。
62 0
|
21天前
|
开发者
鸿蒙开发:资讯项目实战之项目初始化搭建
目前来说,我们的资讯项目只是往前迈了很小的一步,仅仅实现了项目创建,步虽小,但概念性的知识很多,这也是这个项目的初衷,让大家不仅仅可以掌握日常的技术开发,也能让大家理解实际的项目开发知识。
鸿蒙开发:资讯项目实战之项目初始化搭建
|
15天前
|
缓存 JavaScript IDE
鸿蒙开发:基于最新API,如何实现组件化运行
手动只是让大家了解切换的原理,在实际开发中,可不推荐手动,下篇文章,我们将通过脚本或者插件,快速实现组件化模块之间的切换,实现独立运行,敬请期待!
鸿蒙开发:基于最新API,如何实现组件化运行
|
21天前
|
SQL 弹性计算 数据库
鸿蒙5开发宝藏案例分享---优化应用时延问题
鸿蒙性能优化指南来了!从UI渲染到数据库操作,6大实战案例助你提升应用流畅度。布局层级优化、数据加载并发、数据库查询提速、相机资源延迟释放、手势识别灵敏调整及转场动画精调,全面覆盖性能痛点。附赠性能自检清单,帮助开发者高效定位问题,让应用运行如飞!来自华为官方文档的精华内容,建议收藏并反复研读,共同探讨更多优化技巧。
|
21天前
|
缓存
鸿蒙5开发宝藏案例分享---Swiper组件性能优化实战
本文分享了鸿蒙系统中Swiper组件的性能优化技巧,包括:1) 使用`LazyForEach`替代`ForEach`实现懒加载,显著降低内存占用;2) 通过`cachedCount`精准控制缓存数量,平衡流畅度与内存消耗;3) 利用`onAnimationStart`在抛滑时提前加载资源,提升构建效率;4) 添加`@Reusable`装饰器复用组件实例,减少创建开销。实际应用后,图库页帧率从45fps提升至58fps,效果显著。适合处理复杂列表或轮播场景,欢迎交流经验!
|
21天前
|
缓存 JavaScript 前端开发
鸿蒙5开发宝藏案例分享---Web开发优化案例分享
本文深入解读鸿蒙官方文档中的 `ArkWeb` 性能优化技巧,从预启动进程到预渲染,涵盖预下载、预连接、预取POST等八大优化策略。通过代码示例详解如何提升Web页面加载速度,助你打造流畅的HarmonyOS应用体验。内容实用,按需选用,让H5页面快到飞起!
|
21天前
|
数据库
鸿蒙5开发宝藏案例分享---跨线程性能优化指南
本文深入探讨鸿蒙系统跨线程序列化性能优化,借助DevEco Profiler工具定位序列化瓶颈。通过Sendable接口改造、数据瘦身等方法,将5万本书对象的序列化耗时从260ms+降至&lt;8ms,甚至&lt;1ms。总结避坑经验,建议常态化使用Profiler检测,避免传递大对象,提升多线程开发效率。