[HarmonyOS NEXT 实战案例十六] 个人资料卡片网格布局(下)

2025-06-08 15:07:27
107次阅读
0个评论
最后修改时间:2025-06-08 15:17:17

[HarmonyOS NEXT 实战案例十六] 个人资料卡片网格布局(下)

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

效果演示

image.png

1. 概述

在上一篇教程中,我们学习了如何使用HarmonyOS NEXT的GridRow和GridCol组件实现基础的个人资料卡片网格布局。本篇教程将在此基础上,深入探讨如何优化和扩展个人资料卡片,实现更加灵活、美观和功能丰富的界面。

本教程将涵盖以下内容:

  • 响应式布局设计
  • 个人资料卡片的交互设计
  • 动画效果实现
  • 主题与样式定制
  • 数据统计卡片的高级实现
  • GridRow和GridCol的高级配置

2. 响应式布局设计

2.1 断点配置

为了使个人资料卡片能够适应不同屏幕尺寸的设备,我们需要配置断点和响应式列数:

// 头像和基本信息
GridRow({ 
    columns: { xs: 1, sm: 1, md: 1, lg: 1 },
    breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
    // ...
}

// 数据统计
GridRow({ 
    columns: { xs: 2, sm: 4, md: 4, lg: 4 },
    breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
    // ...
}

// 操作按钮
GridRow({ 
    columns: { xs: 1, sm: 2, md: 2, lg: 2 },
    gutter: { x: 16, y: 0 },
    breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
    // ...
}

在这个配置中:

  • 头像和基本信息区域在所有断点下都保持单列布局
  • 数据统计区域在xs断点(小屏设备)下使用2列布局,在其他断点下使用4列布局
  • 操作按钮区域在xs断点下使用单列布局,在其他断点下使用2列布局
  • 所有GridRow组件都使用相同的断点配置,确保布局变化的一致性

2.2 自定义断点

除了使用默认的断点配置外,我们还可以根据具体需求自定义断点:

// 自定义断点配置
const customBreakpoints = {
    value: ['320vp', '520vp', '720vp', '960vp'],
    reference: BreakpointsReference.WindowSize
};

// 使用自定义断点
GridRow({ 
    columns: { xs: 2, sm: 2, md: 4, lg: 4, xl: 4 },
    breakpoints: customBreakpoints
}) {
    // ...
}

通过自定义断点,我们可以更精细地控制布局在不同屏幕尺寸下的表现。

2.3 头像和基本信息的响应式布局

在小屏设备上,我们可以调整头像和基本信息的布局,使其更加紧凑:

@Builder
private ProfileHeader(isSmallScreen: boolean) {
    Row() {
        Image($r('app.media.phone'))
            .width(isSmallScreen ? 60 : 80)
            .height(isSmallScreen ? 60 : 80)
            .borderRadius(isSmallScreen ? 30 : 40)
            .margin({ right: isSmallScreen ? 12 : 16 })

        Column() {
            Text('张三')
                .fontSize(isSmallScreen ? 18 : 20)
                .fontWeight(FontWeight.Bold)

            Text('HarmonyOS开发者')
                .fontSize(isSmallScreen ? 12 : 14)
                .fontColor('#9E9E9E')
                .margin({ top: 4 })

            Text('北京 · 海淀')
                .fontSize(isSmallScreen ? 10 : 12)
                .fontColor('#9E9E9E')
                .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
    }
    .padding(isSmallScreen ? 12 : 16)
}

然后在build方法中使用媒体查询来判断当前屏幕尺寸:

build() {
    Column() {
        // 使用媒体查询判断屏幕尺寸
        MediaQuery({ minWidth: 320, maxWidth: 520 }) {
            GridRow({ columns: 1 }) {
                GridCol({ span: 1 }) {
                    this.ProfileHeader(true) // 小屏版本
                }
            }
        }
        
        MediaQuery({ minWidth: 521 }) {
            GridRow({ columns: 1 }) {
                GridCol({ span: 1 }) {
                    this.ProfileHeader(false) // 大屏版本
                }
            }
        }
        
        // 其他部分...
    }
}

3. 个人资料卡片的交互设计

3.1 点击效果

为数据统计卡片添加点击效果,使其更具交互性:

@Builder
private StatCard(stat: StatusType) {
    Column() {
        Text(stat.value)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)

        Text(stat.label)
            .fontSize(12)
            .fontColor('#9E9E9E')
            .margin({ top: 4 })
    }
    .padding(8)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .onClick(() => {
        // 处理点击事件
        console.info(`${stat.label} clicked: ${stat.value}`)
    })
    .stateStyles({
        pressed: {
            .backgroundColor('#F0F0F0')
            .scale({ x: 0.95, y: 0.95 })
        }
    })
}

然后在GridRow中使用这个Builder:

// 数据统计
GridRow({ columns: { xs: 2, sm: 4, md: 4, lg: 4 } }) {
    ForEach(this.stats, (stat: StatusType) => {
        GridCol({ span: 1 }) {
            this.StatCard(stat)
        }
    })
}

3.2 悬停效果

在桌面设备上,我们可以为卡片添加悬停效果:

.stateStyles({
    pressed: {
        .backgroundColor('#F0F0F0')
        .scale({ x: 0.95, y: 0.95 })
    },
    hover: {
        .backgroundColor('#F8F8F8')
        .shadow({
            radius: 6,
            color: 'rgba(0, 0, 0, 0.1)',
            offsetX: 0,
            offsetY: 2
        })
    }
})

3.3 长按效果

为数据统计卡片添加长按效果,显示详细信息:

@State isLongPressed: boolean = false
@State currentStat: StatusType = { label: '', value: '' }

// 在StatCard中添加长按事件
.gesture(
    LongPressGesture({ repeat: false })
        .onAction(() => {
            this.isLongPressed = true
            this.currentStat = stat
        })
)

// 在build方法末尾添加弹出层
build() {
    Column() {
        // 原有内容...
        
        // 长按弹出层
        if (this.isLongPressed) {
            Panel() {
                Column() {
                    Text(`${this.currentStat.label}详情`)
                        .fontSize(18)
                        .fontWeight(FontWeight.Bold)
                        .margin({ bottom: 16 })
                    
                    Text(`当前${this.currentStat.label}数:${this.currentStat.value}`)
                        .fontSize(16)
                        .margin({ bottom: 8 })
                    
                    Text(`上周增长:12%`)
                        .fontSize(14)
                        .fontColor('#4CAF50')
                        .margin({ bottom: 8 })
                    
                    Button('关闭')
                        .onClick(() => {
                            this.isLongPressed = false
                        })
                        .margin({ top: 16 })
                }
                .padding(16)
            }
            .mode(PanelMode.Half)
            .dragBar(true)
            .onChange((value: number) => {
                if (value === 0) {
                    this.isLongPressed = false
                }
            })
        }
    }
}

4. 动画效果实现

4.1 进入动画

为个人资料卡片添加进入动画,使界面更加生动:

@State appearAnimation: boolean = false

aboutToAppear() {
    // 延迟执行动画,确保组件已经渲染
    setTimeout(() => {
        this.appearAnimation = true
    }, 100)
}

build() {
    Column() {
        // 头像和基本信息
        GridRow({ columns: 1 }) {
            GridCol({ span: 1 }) {
                Row() {
                    // 原有内容...
                }
                .padding(16)
                .opacity(this.appearAnimation ? 1 : 0)
                .animation({
                    duration: 500,
                    curve: Curve.EaseOut,
                    delay: 0,
                    iterations: 1
                })
            }
        }
        
        // 数据统计
        GridRow({ columns: { xs: 2, sm: 4, md: 4, lg: 4 } }) {
            ForEach(this.stats, (stat: StatusType, index: number) => {
                GridCol({ span: 1 }) {
                    this.StatCard(stat)
                        .opacity(this.appearAnimation ? 1 : 0)
                        .animation({
                            duration: 500,
                            curve: Curve.EaseOut,
                            delay: 100 + index * 50,
                            iterations: 1
                        })
                }
            })
        }
        
        // 操作按钮
        GridRow({ columns: { xs: 1, sm: 2, md: 2, lg: 2 }, gutter: 16 }) {
            GridCol({ span: 1 }) {
                Button('编辑资料')
                    // 原有内容...
                    .opacity(this.appearAnimation ? 1 : 0)
                    .animation({
                        duration: 500,
                        curve: Curve.EaseOut,
                        delay: 300,
                        iterations: 1
                    })
            }
            
            GridCol({ span: 1 }) {
                Button('设置')
                    // 原有内容...
                    .opacity(this.appearAnimation ? 1 : 0)
                    .animation({
                        duration: 500,
                        curve: Curve.EaseOut,
                        delay: 350,
                        iterations: 1
                    })
            }
        }
    }
}

4.2 点击动画

为按钮添加点击动画,提升交互体验:

@Builder
private AnimatedButton(text: string, isNormal: boolean = false) {
    Button(text)
        .width('100%')
        .fontSize(16)
        .type(isNormal ? ButtonType.Normal : ButtonType.Normal)
        .stateStyles({
            pressed: {
                .scale({ x: 0.95, y: 0.95 })
                .opacity(0.8)
            }
        })
        .animation({
            duration: 200,
            curve: Curve.EaseInOut,
            iterations: 1
        })
}

// 在GridRow中使用这个Builder
GridRow({ columns: { xs: 1, sm: 2, md: 2, lg: 2 }, gutter: 16 }) {
    GridCol({ span: 1 }) {
        this.AnimatedButton('编辑资料')
    }
    
    GridCol({ span: 1 }) {
        this.AnimatedButton('设置', true)
    }
}

5. 主题与样式定制

5.1 卡片样式变体

为数据统计卡片创建不同的样式变体:

enum StatCardStyle {
    Default,
    Gradient,
    Outlined
}

@Builder
private StatCard(stat: StatusType, style: StatCardStyle = StatCardStyle.Default) {
    Column() {
        Text(stat.value)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(style === StatCardStyle.Gradient ? '#FFFFFF' : '#000000')

        Text(stat.label)
            .fontSize(12)
            .fontColor(style === StatCardStyle.Gradient ? 'rgba(255, 255, 255, 0.8)' : '#9E9E9E')
            .margin({ top: 4 })
    }
    .padding(8)
    .backgroundColor(this.getCardBackground(stat, style))
    .borderRadius(8)
    .border(style === StatCardStyle.Outlined ? {
        width: 1,
        color: '#E0E0E0',
        style: BorderStyle.Solid
    } : null)
    // 其他属性...
}

private getCardBackground(stat: StatusType, style: StatCardStyle): ResourceColor | string {
    if (style === StatCardStyle.Default) {
        return '#FFFFFF';
    } else if (style === StatCardStyle.Gradient) {
        // 根据统计项类型返回不同的渐变色
        switch (stat.label) {
            case '关注':
                return 'linear-gradient(135deg, #4CAF50, #8BC34A)';
            case '粉丝':
                return 'linear-gradient(135deg, #2196F3, #03A9F4)';
            case '获赞':
                return 'linear-gradient(135deg, #FF5722, #FF9800)';
            case '收藏':
                return 'linear-gradient(135deg, #9C27B0, #E91E63)';
            default:
                return '#FFFFFF';
        }
    } else {
        return '#FFFFFF';
    }
}

// 在GridRow中使用不同的样式
GridRow({ columns: { xs: 2, sm: 4, md: 4, lg: 4 } }) {
    ForEach(this.stats, (stat: StatusType, index: number) => {
        GridCol({ span: 1 }) {
            // 根据索引使用不同的样式
            this.StatCard(stat, index % 2 === 0 ? StatCardStyle.Gradient : StatCardStyle.Default)
        }
    })
}

5.2 自定义主题

创建自定义主题,使个人资料卡片能够适应不同的应用风格:

@Observed
class ProfileTheme {
    primaryColor: string = '#2196F3'
    secondaryColor: string = '#FF5722'
    backgroundColor: string = '#F5F5F5'
    cardBackgroundColor: string = '#FFFFFF'
    textPrimaryColor: string = '#000000'
    textSecondaryColor: string = '#9E9E9E'
    borderRadius: number = 8
    isDark: boolean = false
    
    // 切换暗色主题
    toggleDarkMode() {
        this.isDark = !this.isDark
        if (this.isDark) {
            this.backgroundColor = '#121212'
            this.cardBackgroundColor = '#1E1E1E'
            this.textPrimaryColor = '#FFFFFF'
            this.textSecondaryColor = 'rgba(255, 255, 255, 0.7)'
        } else {
            this.backgroundColor = '#F5F5F5'
            this.cardBackgroundColor = '#FFFFFF'
            this.textPrimaryColor = '#000000'
            this.textSecondaryColor = '#9E9E9E'
        }
    }
}

@Component
export struct ProfileGrid {
    @Provide theme: ProfileTheme = new ProfileTheme()
    // 其他属性...
    
    build() {
        Column() {
            // 主题切换按钮
            Row() {
                Text('切换主题')
                    .fontSize(14)
                    .fontColor(this.theme.textPrimaryColor)
                
                Toggle({ type: ToggleType.Switch, isOn: this.theme.isDark })
                    .onChange(() => {
                        this.theme.toggleDarkMode()
                    })
            }
            .width('100%')
            .justifyContent(FlexAlign.End)
            .margin({ bottom: 16 })
            
            // 其他内容,使用主题属性...
            GridRow({ columns: 1 }) {
                GridCol({ span: 1 }) {
                    Row() {
                        // 使用主题属性
                        Image($r('app.media.phone'))
                            // ...
                        
                        Column() {
                            Text('张三')
                                .fontSize(20)
                                .fontWeight(FontWeight.Bold)
                                .fontColor(this.theme.textPrimaryColor)
                            
                            Text('HarmonyOS开发者')
                                .fontSize(14)
                                .fontColor(this.theme.textSecondaryColor)
                                .margin({ top: 4 })
                            
                            Text('北京 · 海淀')
                                .fontSize(12)
                                .fontColor(this.theme.textSecondaryColor)
                                .margin({ top: 4 })
                        }
                        .alignItems(HorizontalAlign.Start)
                    }
                    .padding(16)
                    .backgroundColor(this.theme.cardBackgroundColor)
                    .borderRadius(this.theme.borderRadius)
                }
            }
            
            // 其他内容...
        }
        .width('100%')
        .padding(16)
        .backgroundColor(this.theme.backgroundColor)
    }
}

6. 数据统计卡片的高级实现

6.1 带图表的统计卡片

为数据统计卡片添加简单的图表,使数据可视化更加直观:

// 扩展StatusType接口
interface StatusType {
    label: string;
    value: string;
    trend: number[]; // 趋势数据
    change: number; // 变化百分比
}

// 更新模拟数据
private stats: StatusType[] = [
    { label: '关注', value: '128', trend: [10, 15, 12, 18, 20, 25, 30], change: 15 },
    { label: '粉丝', value: '1.2K', trend: [100, 120, 150, 200, 250, 300, 350], change: 25 },
    { label: '获赞', value: '3.4K', trend: [300, 320, 350, 400, 420, 450, 500], change: 10 },
    { label: '收藏', value: '256', trend: [20, 25, 30, 35, 40, 45, 50], change: 20 }
]

@Builder
private StatCardWithChart(stat: StatusType) {
    Column() {
        Row() {
            Column() {
                Text(stat.value)
                    .fontSize(18)
                    .fontWeight(FontWeight.Bold)

                Text(stat.label)
                    .fontSize(12)
                    .fontColor('#9E9E9E')
                    .margin({ top: 4 })
                
                Row() {
                    if (stat.change > 0) {
                        Image($r('app.media.arrow_up'))
                            .width(12)
                            .height(12)
                            .fillColor('#4CAF50')
                            .margin({ right: 4 })
                    } else if (stat.change < 0) {
                        Image($r('app.media.arrow_down'))
                            .width(12)
                            .height(12)
                            .fillColor('#F44336')
                            .margin({ right: 4 })
                    }
                    
                    Text(`${Math.abs(stat.change)}%`)
                        .fontSize(10)
                        .fontColor(stat.change > 0 ? '#4CAF50' : (stat.change < 0 ? '#F44336' : '#9E9E9E'))
                }
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            
            // 简单的折线图
            Row() {
                ForEach(stat.trend, (value: number, index: number) => {
                    Column() {
                        // 计算高度比例
                        let maxValue = Math.max(...stat.trend)
                        let height = 30 * (value / maxValue)
                        
                        Column()
                            .width(4)
                            .height(height)
                            .backgroundColor(this.getChartColor(stat.label))
                            .borderRadius(2)
                    }
                    .height(30)
                    .justifyContent(FlexAlign.End)
                    .margin({ right: index < stat.trend.length - 1 ? 2 : 0 })
                })
            }
            .margin({ left: 8 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
    }
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    // 其他属性...
}

private getChartColor(label: string): string {
    switch (label) {
        case '关注':
            return '#4CAF50';
        case '粉丝':
            return '#2196F3';
        case '获赞':
            return '#FF5722';
        case '收藏':
            return '#9C27B0';
        default:
            return '#000000';
    }
}

6.2 带进度指示器的统计卡片

为数据统计卡片添加进度指示器,显示目标完成情况:

// 扩展StatusType接口
interface StatusType {
    label: string;
    value: string;
    target: string; // 目标值
    progress: number; // 完成进度(0-1)
}

// 更新模拟数据
private stats: StatusType[] = [
    { label: '关注', value: '128', target: '200', progress: 0.64 },
    { label: '粉丝', value: '1.2K', target: '2K', progress: 0.6 },
    { label: '获赞', value: '3.4K', target: '5K', progress: 0.68 },
    { label: '收藏', value: '256', target: '500', progress: 0.51 }
]

@Builder
private StatCardWithProgress(stat: StatusType) {
    Column() {
        Row() {
            Text(stat.value)
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
            
            Text(` / ${stat.target}`)
                .fontSize(12)
                .fontColor('#9E9E9E')
        }
        
        Text(stat.label)
            .fontSize(12)
            .fontColor('#9E9E9E')
            .margin({ top: 4, bottom: 8 })
        
        Progress({ value: stat.progress * 100, total: 100, type: ProgressType.Linear })
            .color(this.getProgressColor(stat.label))
            .height(4)
        
        Text(`${Math.round(stat.progress * 100)}%`)
            .fontSize(10)
            .fontColor('#9E9E9E')
            .margin({ top: 4 })
            .textAlign(TextAlign.End)
            .width('100%')
    }
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    // 其他属性...
}

private getProgressColor(label: string): string {
    // 与getChartColor相同的实现
}

7. GridRow和GridCol的高级配置

7.1 嵌套网格

使用嵌套的GridRow和GridCol实现更复杂的布局:

// 在数据统计区域使用嵌套网格
GridRow({ columns: { xs: 1, sm: 2, md: 2, lg: 2 }, gutter: 16 }) {
    // 左侧两个统计项
    GridCol({ span: 1 }) {
        GridRow({ columns: 2, gutter: 8 }) {
            GridCol({ span: 1 }) {
                this.StatCard(this.stats[0])
            }
            
            GridCol({ span: 1 }) {
                this.StatCard(this.stats[1])
            }
        }
    }
    
    // 右侧两个统计项
    GridCol({ span: 1 }) {
        GridRow({ columns: 2, gutter: 8 }) {
            GridCol({ span: 1 }) {
                this.StatCard(this.stats[2])
            }
            
            GridCol({ span: 1 }) {
                this.StatCard(this.stats[3])
            }
        }
    }
}

7.2 列偏移

使用offset属性实现列偏移,创建不对称的布局:

// 使用列偏移创建不对称布局
GridRow({ columns: 12, gutter: 8 }) {
    GridCol({ span: 3 }) {
        this.StatCard(this.stats[0])
    }
    
    GridCol({ span: 3, offset: 1 }) {
        this.StatCard(this.stats[1])
    }
    
    GridCol({ span: 4, offset: 1 }) {
        this.StatCard(this.stats[2])
    }
}

7.3 列顺序调整

使用order属性调整列的顺序:

// 使用order属性调整列顺序
GridRow({ columns: 4, gutter: 8 }) {
    GridCol({ span: 1, order: 4 }) {
        this.StatCard(this.stats[0])
    }
    
    GridCol({ span: 1, order: 3 }) {
        this.StatCard(this.stats[1])
    }
    
    GridCol({ span: 1, order: 2 }) {
        this.StatCard(this.stats[2])
    }
    
    GridCol({ span: 1, order: 1 }) {
        this.StatCard(this.stats[3])
    }
}

8. 完整优化代码

以下是结合了响应式布局、交互设计、动画效果和主题定制的完整优化代码:

// 个人资料卡片网格布局(优化版)
@Observed
class ProfileTheme {
    primaryColor: string = '#2196F3'
    secondaryColor: string = '#FF5722'
    backgroundColor: string = '#F5F5F5'
    cardBackgroundColor: string = '#FFFFFF'
    textPrimaryColor: string = '#000000'
    textSecondaryColor: string = '#9E9E9E'
    borderRadius: number = 8
    isDark: boolean = false
    
    // 切换暗色主题
    toggleDarkMode() {
        this.isDark = !this.isDark
        if (this.isDark) {
            this.backgroundColor = '#121212'
            this.cardBackgroundColor = '#1E1E1E'
            this.textPrimaryColor = '#FFFFFF'
            this.textSecondaryColor = 'rgba(255, 255, 255, 0.7)'
        } else {
            this.backgroundColor = '#F5F5F5'
            this.cardBackgroundColor = '#FFFFFF'
            this.textPrimaryColor = '#000000'
            this.textSecondaryColor = '#9E9E9E'
        }
    }
}

interface StatusType {
    label: string;
    value: string;
    trend: number[];
    change: number;
}

enum StatCardStyle {
    Default,
    Gradient,
    Outlined
}

@Component
export struct ProfileGrid {
    @Provide theme: ProfileTheme = new ProfileTheme()
    @State appearAnimation: boolean = false
    @State isLongPressed: boolean = false
    @State currentStat: StatusType = { label: '', value: '', trend: [], change: 0 }
    
    private stats: StatusType[] = [
        { label: '关注', value: '128', trend: [10, 15, 12, 18, 20, 25, 30], change: 15 },
        { label: '粉丝', value: '1.2K', trend: [100, 120, 150, 200, 250, 300, 350], change: 25 },
        { label: '获赞', value: '3.4K', trend: [300, 320, 350, 400, 420, 450, 500], change: 10 },
        { label: '收藏', value: '256', trend: [20, 25, 30, 35, 40, 45, 50], change: 20 }
    ]
    
    aboutToAppear() {
        // 延迟执行动画,确保组件已经渲染
        setTimeout(() => {
            this.appearAnimation = true
        }, 100)
    }
    
    @Builder
    private ProfileHeader(isSmallScreen: boolean) {
        Row() {
            Image($r('app.media.phone'))
                .width(isSmallScreen ? 60 : 80)
                .height(isSmallScreen ? 60 : 80)
                .borderRadius(isSmallScreen ? 30 : 40)
                .margin({ right: isSmallScreen ? 12 : 16 })

            Column() {
                Text('张三')
                    .fontSize(isSmallScreen ? 18 : 20)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(this.theme.textPrimaryColor)

                Text('HarmonyOS开发者')
                    .fontSize(isSmallScreen ? 12 : 14)
                    .fontColor(this.theme.textSecondaryColor)
                    .margin({ top: 4 })

                Text('北京 · 海淀')
                    .fontSize(isSmallScreen ? 10 : 12)
                    .fontColor(this.theme.textSecondaryColor)
                    .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
        }
        .padding(isSmallScreen ? 12 : 16)
        .backgroundColor(this.theme.cardBackgroundColor)
        .borderRadius(this.theme.borderRadius)
    }
    
    @Builder
    private StatCardWithChart(stat: StatusType, style: StatCardStyle = StatCardStyle.Default) {
        Column() {
            Row() {
                Column() {
                    Text(stat.value)
                        .fontSize(18)
                        .fontWeight(FontWeight.Bold)
                        .fontColor(style === StatCardStyle.Gradient ? '#FFFFFF' : this.theme.textPrimaryColor)

                    Text(stat.label)
                        .fontSize(12)
                        .fontColor(style === StatCardStyle.Gradient ? 'rgba(255, 255, 255, 0.8)' : this.theme.textSecondaryColor)
                        .margin({ top: 4 })
                    
                    Row() {
                        if (stat.change > 0) {
                            Image($r('app.media.arrow_up'))
                                .width(12)
                                .height(12)
                                .fillColor('#4CAF50')
                                .margin({ right: 4 })
                        } else if (stat.change < 0) {
                            Image($r('app.media.arrow_down'))
                                .width(12)
                                .height(12)
                                .fillColor('#F44336')
                                .margin({ right: 4 })
                        }
                        
                        Text(`${Math.abs(stat.change)}%`)
                            .fontSize(10)
                            .fontColor(stat.change > 0 ? '#4CAF50' : (stat.change < 0 ? '#F44336' : this.theme.textSecondaryColor))
                    }
                    .margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Start)
                
                // 简单的折线图
                Row() {
                    ForEach(stat.trend, (value: number, index: number) => {
                        Column() {
                            // 计算高度比例
                            let maxValue = Math.max(...stat.trend)
                            let height = 30 * (value / maxValue)
                            
                            Column()
                                .width(4)
                                .height(height)
                                .backgroundColor(this.getChartColor(stat.label))
                                .borderRadius(2)
                        }
                        .height(30)
                        .justifyContent(FlexAlign.End)
                        .margin({ right: index < stat.trend.length - 1 ? 2 : 0 })
                    })
                }
                .margin({ left: 8 })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)
        }
        .padding(12)
        .backgroundColor(this.getCardBackground(stat, style))
        .borderRadius(this.theme.borderRadius)
        .border(style === StatCardStyle.Outlined ? {
            width: 1,
            color: '#E0E0E0',
            style: BorderStyle.Solid
        } : null)
        .onClick(() => {
            // 处理点击事件
            console.info(`${stat.label} clicked: ${stat.value}`)
        })
        .gesture(
            LongPressGesture({ repeat: false })
                .onAction(() => {
                    this.isLongPressed = true
                    this.currentStat = stat
                })
        )
        .stateStyles({
            pressed: {
                .backgroundColor(style === StatCardStyle.Gradient ? this.getCardBackground(stat, style) : '#F0F0F0')
                .scale({ x: 0.95, y: 0.95 })
                .opacity(0.9)
            },
            hover: {
                .backgroundColor(style === StatCardStyle.Gradient ? this.getCardBackground(stat, style) : '#F8F8F8')
                .shadow({
                    radius: 6,
                    color: 'rgba(0, 0, 0, 0.1)',
                    offsetX: 0,
                    offsetY: 2
                })
            }
        })
    }
    
    @Builder
    private AnimatedButton(text: string, isNormal: boolean = false) {
        Button(text)
            .width('100%')
            .fontSize(16)
            .type(isNormal ? ButtonType.Normal : ButtonType.Normal)
            .backgroundColor(isNormal ? 'transparent' : this.theme.primaryColor)
            .fontColor(isNormal ? this.theme.primaryColor : '#FFFFFF')
            .stateStyles({
                pressed: {
                    .scale({ x: 0.95, y: 0.95 })
                    .opacity(0.8)
                }
            })
            .animation({
                duration: 200,
                curve: Curve.EaseInOut,
                iterations: 1
            })
    }
    
    private getCardBackground(stat: StatusType, style: StatCardStyle): ResourceColor | string {
        if (style === StatCardStyle.Default) {
            return this.theme.cardBackgroundColor;
        } else if (style === StatCardStyle.Gradient) {
            // 根据统计项类型返回不同的渐变色
            switch (stat.label) {
                case '关注':
                    return 'linear-gradient(135deg, #4CAF50, #8BC34A)';
                case '粉丝':
                    return 'linear-gradient(135deg, #2196F3, #03A9F4)';
                case '获赞':
                    return 'linear-gradient(135deg, #FF5722, #FF9800)';
                case '收藏':
                    return 'linear-gradient(135deg, #9C27B0, #E91E63)';
                default:
                    return this.theme.cardBackgroundColor;
            }
        } else {
            return this.theme.cardBackgroundColor;
        }
    }
    
    private getChartColor(label: string): string {
        switch (label) {
            case '关注':
                return '#4CAF50';
            case '粉丝':
                return '#2196F3';
            case '获赞':
                return '#FF5722';
            case '收藏':
                return '#9C27B0';
            default:
                return '#000000';
        }
    }
    
    build() {
        Column() {
            // 主题切换按钮
            Row() {
                Text('切换主题')
                    .fontSize(14)
                    .fontColor(this.theme.textPrimaryColor)
                
                Toggle({ type: ToggleType.Switch, isOn: this.theme.isDark })
                    .onChange(() => {
                        this.theme.toggleDarkMode()
                    })
            }
            .width('100%')
            .justifyContent(FlexAlign.End)
            .margin({ bottom: 16 })
            
            // 使用媒体查询判断屏幕尺寸
            MediaQuery({ minWidth: 320, maxWidth: 520 }) {
                GridRow({ columns: 1 }) {
                    GridCol({ span: 1 }) {
                        this.ProfileHeader(true) // 小屏版本
                    }
                }
                .opacity(this.appearAnimation ? 1 : 0)
                .animation({
                    duration: 500,
                    curve: Curve.EaseOut,
                    delay: 0,
                    iterations: 1
                })
            }
            
            MediaQuery({ minWidth: 521 }) {
                GridRow({ columns: 1 }) {
                    GridCol({ span: 1 }) {
                        this.ProfileHeader(false) // 大屏版本
                    }
                }
                .opacity(this.appearAnimation ? 1 : 0)
                .animation({
                    duration: 500,
                    curve: Curve.EaseOut,
                    delay: 0,
                    iterations: 1
                })
            }
            
            // 数据统计
            GridRow({ 
                columns: { xs: 2, sm: 4, md: 4, lg: 4 },
                gutter: { x: 16, y: 16 },
                breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
            }) {
                ForEach(this.stats, (stat: StatusType, index: number) => {
                    GridCol({ span: 1 }) {
                        // 根据索引使用不同的样式
                        this.StatCardWithChart(stat, index % 2 === 0 ? StatCardStyle.Gradient : StatCardStyle.Default)
                            .opacity(this.appearAnimation ? 1 : 0)
                            .animation({
                                duration: 500,
                                curve: Curve.EaseOut,
                                delay: 100 + index * 50,
                                iterations: 1
                            })
                    }
                })
            }
            .margin({ top: 16 })
            
            // 操作按钮
            GridRow({ 
                columns: { xs: 1, sm: 2, md: 2, lg: 2 }, 
                gutter: { x: 16, y: 16 },
                breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
            }) {
                GridCol({ span: 1 }) {
                    this.AnimatedButton('编辑资料')
                        .opacity(this.appearAnimation ? 1 : 0)
                        .animation({
                            duration: 500,
                            curve: Curve.EaseOut,
                            delay: 300,
                            iterations: 1
                        })
                }
                
                GridCol({ span: 1 }) {
                    this.AnimatedButton('设置', true)
                        .opacity(this.appearAnimation ? 1 : 0)
                        .animation({
                            duration: 500,
                            curve: Curve.EaseOut,
                            delay: 350,
                            iterations: 1
                        })
                }
            }
            .margin({ top: 24 })
            
            // 长按弹出层
            if (this.isLongPressed) {
                Panel() {
                    Column() {
                        Text(`${this.currentStat.label}详情`)
                            .fontSize(18)
                            .fontWeight(FontWeight.Bold)
                            .fontColor(this.theme.textPrimaryColor)
                            .margin({ bottom: 16 })
                        
                        Text(`当前${this.currentStat.label}数:${this.currentStat.value}`)
                            .fontSize(16)
                            .fontColor(this.theme.textPrimaryColor)
                            .margin({ bottom: 8 })
                        
                        Text(`上周增长:${this.currentStat.change}%`)
                            .fontSize(14)
                            .fontColor(this.currentStat.change > 0 ? '#4CAF50' : (this.currentStat.change < 0 ? '#F44336' : this.theme.textSecondaryColor))
                            .margin({ bottom: 8 })
                        
                        // 趋势图表
                        Row() {
                            ForEach(this.currentStat.trend, (value: number, index: number) => {
                                Column() {
                                    // 计算高度比例
                                    let maxValue = Math.max(...this.currentStat.trend)
                                    let height = 100 * (value / maxValue)
                                    
                                    Column()
                                        .width(20)
                                        .height(height)
                                        .backgroundColor(this.getChartColor(this.currentStat.label))
                                        .borderRadius(4)
                                    
                                    Text(`${index + 1}日`)
                                        .fontSize(10)
                                        .fontColor(this.theme.textSecondaryColor)
                                        .margin({ top: 4 })
                                }
                                .height(120)
                                .justifyContent(FlexAlign.End)
                                .alignItems(HorizontalAlign.Center)
                                .margin({ right: index < this.currentStat.trend.length - 1 ? 8 : 0 })
                            })
                        }
                        .width('100%')
                        .justifyContent(FlexAlign.SpaceEvenly)
                        .margin({ top: 16, bottom: 24 })
                        
                        Button('关闭')
                            .onClick(() => {
                                this.isLongPressed = false
                            })
                            .backgroundColor(this.theme.primaryColor)
                            .width('100%')
                    }
                    .padding(16)
                    .backgroundColor(this.theme.backgroundColor)
                }
                .mode(PanelMode.Half)
                .dragBar(true)
                .backgroundColor(this.theme.backgroundColor)
                .onChange((value: number) => {
                    if (value === 0) {
                        this.isLongPressed = false
                    }
                })
            }
        }
        .width('100%')
        .padding(16)
        .backgroundColor(this.theme.backgroundColor)
    }
}

9. 响应式布局最佳实践

在实现响应式布局时,可以遵循以下最佳实践:

9.1 移动优先设计

从移动设备开始设计,然后逐步扩展到更大的屏幕尺寸。这种方法可以确保在小屏设备上的良好体验,同时在大屏设备上充分利用额外的空间。

9.2 使用相对单位

使用相对单位(如vp、fp)而不是固定像素值,使布局能够适应不同的屏幕尺寸和分辨率。

9.3 合理设置断点

根据目标设备的常见屏幕尺寸设置断点,确保布局在各种设备上都能正常显示。

9.4 测试不同设备

在不同尺寸的设备上测试应用,确保布局在各种条件下都能正常工作。

10. 总结

本教程详细讲解了如何优化个人资料卡片网格布局,添加响应式支持、交互设计、动画效果和主题定制。通过使用HarmonyOS NEXT的GridRow和GridCol组件的高级特性,我们实现了一个功能丰富、美观灵活的个人资料卡片。

主要内容包括:

  • 响应式布局设计,使卡片能够适应不同屏幕尺寸的设备
  • 个人资料卡片的交互设计,包括点击效果、悬停效果和长按效果
  • 动画效果实现,包括进入动画和点击动画
  • 主题与样式定制,包括卡片样式变体和自定义主题
  • 数据统计卡片的高级实现,包括带图表的统计卡片和带进度指示器的统计卡片
  • GridRow和GridCol的高级配置,包括嵌套网格、列偏移和列顺序调整

通过本教程,你应该已经掌握了如何使用HarmonyOS NEXT的GridRow和GridCol组件实现复杂的网格布局,以及如何添加各种高级特性,提升用户体验。这些技能可以应用到各种需要网格布局的场景中,如个人资料页、数据仪表盘、设置页面等。

收藏00

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