[HarmonyOS NEXT 实战案例十六] 个人资料卡片网格布局(下)
[HarmonyOS NEXT 实战案例十六] 个人资料卡片网格布局(下)
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
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组件实现复杂的网格布局,以及如何添加各种高级特性,提升用户体验。这些技能可以应用到各种需要网格布局的场景中,如个人资料页、数据仪表盘、设置页面等。
- 0回答
- 3粉丝
- 0关注
- [HarmonyOS NEXT 实战案例十六] 个人资料卡片网格布局(上)
- [HarmonyOS NEXT 实战案例四] 天气应用网格布局(下)
- [HarmonyOS NEXT 实战案例六] 餐饮菜单网格布局(下)
- [HarmonyOS NEXT 实战案例七] 健身课程网格布局(下)
- [HarmonyOS NEXT 实战案例九] 旅游景点网格布局(下)
- [HarmonyOS NEXT 实战案例十] 电子书网格布局(下)
- [HarmonyOS NEXT 实战案例十四] 任务管理看板网格布局(下)
- [HarmonyOS NEXT 实战案例十七] 设置选项列表网格布局(下)
- [HarmonyOS NEXT 实战案例五] 社交应用照片墙网格布局(下)
- [HarmonyOS NEXT 实战案例一] 电商首页商品网格布局(下)
- [HarmonyOS NEXT 实战案例十三] 音乐播放器网格布局(下)
- [HarmonyOS NEXT 实战案例八] 电影票务网格布局(下)
- [HarmonyOS NEXT 实战案例十二] 健康数据仪表盘网格布局(下)
- [HarmonyOS NEXT 实战案例四] 天气应用网格布局(上)
- [HarmonyOS NEXT 实战案例六] 餐饮菜单网格布局(上)