[鸿蒙开发实战篇]HarmonyOS 响应式布局实战教程
在.borderRadius(16) // 更大的圆角.border({width: 2, // 更宽的边框color: card.color // 使用卡片的主题色})GridRow({gutter: { x: 16, y: 16 } // 横向和纵向不同间距})通过本教程,你学会了:✅GridRow/GridCol 基础用法✅响应式网格布局实现✅深色/浅色主题适配✅卡片组件设计✅交互效果实现。
HarmonyOS 响应式布局实战教程
📚 教程简介
本教程将手把手教你在 HarmonyOS 应用中实现响应式布局。通过一个完整的功能卡片网格页面示例,你将学会:
- ✅ 使用
GridRow和GridCol实现网格布局 - ✅ 自动适配不同屏幕尺寸
- ✅ 支持深色/浅色主题切换
- ✅ 优雅的卡片设计和交互效果
难度等级:⭐⭐ 入门级
预计时间:20 分钟
运行环境:DevEco Studio 6.0+
🎯 最终效果
我们将创建一个功能卡片网格页面,包含:
- 🔲 3列自适应网格布局(手机端)
- 🎨 深色/浅色主题自动切换
- 📊 6个功能卡片,带图标和描述
- ✨ 点击卡片的交互效果
![效果图示意]
┌──────────────────────────────┐
│ 响应式功能卡片示例 │
│ │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │图标│ │图标│ │图标│ │
│ │功能│ │功能│ │功能│ │
│ └────┘ └────┘ └────┘ │
│ │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │图标│ │图标│ │图标│ │
│ │功能│ │功能│ │功能│ │
│ └────┘ └────┘ └────┘ │
└──────────────────────────────┘
📋 准备工作
1. 创建新页面文件
在你的项目中创建文件:entry/src/main/ets/pages/ResponsiveGridDemo.ets
2. 准备图标资源(可选)
如果没有图标资源,我们将使用文字代替。如果有图标,请将其放在:
entry/src/main/resources/base/media/
🚀 Step 1: 创建基础页面结构
复制以下完整代码到 ResponsiveGridDemo.ets 文件:
/*
* 响应式布局示例页面
* 演示如何使用 GridRow 和 GridCol 创建自适应网格布局
*/
import { router } from '@kit.ArkUI'
import { promptAction } from '@kit.ArkUI'
// 定义功能卡片数据结构
interface FunctionCard {
id: string
title: string
description: string
icon: string // 使用 emoji 或文字代替图标
color: string
}
@Entry
@Component
struct ResponsiveGridDemo {
// 主题状态(支持深色/浅色模式)
@StorageProp('app_is_dark_mode') isDarkMode: boolean = false
// 功能卡片数据
@State cards: FunctionCard[] = []
// 页面初始化
aboutToAppear() {
this.initCards()
}
// 初始化卡片数据
private initCards(): void {
this.cards = [
{
id: 'card1',
title: '数据统计',
description: '查看详细数据',
icon: '📊',
color: '#2196F3'
},
{
id: 'card2',
title: '任务管理',
description: '管理日常任务',
icon: '✅',
color: '#4CAF50'
},
{
id: 'card3',
title: '消息通知',
description: '查看最新消息',
icon: '🔔',
color: '#FF9800'
},
{
id: 'card4',
title: '设置中心',
description: '个性化配置',
icon: '⚙️',
color: '#9C27B0'
},
{
id: 'card5',
title: '帮助文档',
description: '使用指南',
icon: '📖',
color: '#00BCD4'
},
{
id: 'card6',
title: '关于我们',
description: '了解更多',
icon: 'ℹ️',
color: '#607D8B'
}
]
}
// 主布局
build() {
Column() {
// 顶部导航栏
this.buildNavigationBar()
// 滚动内容区域
Scroll() {
Column() {
// 页面标题
this.buildPageTitle()
// 功能卡片网格
this.buildCardGrid()
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 24 })
}
.layoutWeight(1)
.backgroundColor(this.isDarkMode ? '#121212' : '#F5F5F5')
}
.width('100%')
.height('100%')
.backgroundColor(this.isDarkMode ? '#121212' : '#F5F5F5')
}
// 导航栏组件
@Builder
buildNavigationBar() {
Row() {
// 返回按钮
Row() {
Text('←')
.fontSize(24)
.fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
}
.width(40)
.height(40)
.justifyContent(FlexAlign.Center)
.onClick(() => {
router.back()
})
// 标题
Text('响应式布局示例')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
.layoutWeight(1)
.textAlign(TextAlign.Center)
// 占位(保持标题居中)
Row()
.width(40)
.height(40)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(this.isDarkMode ? '#1E1E1E' : '#FFFFFF')
.border({
width: { bottom: 1 },
color: this.isDarkMode ? '#333333' : '#E0E0E0'
})
}
// 页面标题组件
@Builder
buildPageTitle() {
Column() {
Text('功能卡片')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
.margin({ bottom: 8 })
Text('使用 GridRow 和 GridCol 实现的响应式网格布局')
.fontSize(14)
.fontColor(this.isDarkMode ? '#B0BEC5' : '#757575')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding({ top: 24, bottom: 16 })
}
// 卡片网格组件(核心响应式布局)
@Builder
buildCardGrid() {
Column() {
Text('快速功能')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
.width('100%')
.margin({ bottom: 16 })
// 🎯 核心:GridRow 实现响应式网格
GridRow({
columns: 3, // 定义3列
gutter: 12, // 卡片之间的间距
breakpoints: { // 断点配置(可选)
value: ['320vp', '520vp', '840vp'],
reference: BreakpointsReference.WindowWidth
}
}) {
// 遍历所有卡片
ForEach(this.cards, (card: FunctionCard) => {
// 🎯 核心:GridCol 定义每个卡片占据的列数
GridCol({ span: 1 }) {
this.buildCard(card)
}
}, (card: FunctionCard) => card.id)
}
.width('100%')
}
.width('100%')
}
// 单个卡片组件
@Builder
buildCard(card: FunctionCard) {
Column() {
// 图标(使用 emoji)
Text(card.icon)
.fontSize(40)
.margin({ bottom: 12 })
// 标题
Text(card.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ bottom: 4 })
// 描述
Text(card.description)
.fontSize(11)
.fontColor(this.isDarkMode ? '#B0BEC5' : '#757575')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.textAlign(TextAlign.Center)
}
.width('100%')
.padding(16)
.backgroundColor(this.isDarkMode ? '#1E1E1E' : '#FFFFFF')
.borderRadius(12)
.border({
width: 1,
color: this.isDarkMode ? '#424242' : '#E0E0E0'
})
.shadow({
radius: 4,
color: this.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.08)',
offsetX: 0,
offsetY: 2
})
// 点击效果
.onClick(() => {
promptAction.showToast({
message: `点击了 ${card.title}`,
duration: 2000
})
})
// 长按效果(可选)
.onTouch((event) => {
if (event.type === TouchType.Down) {
// 按下时略微缩小
animateTo({ duration: 100 }, () => {
// 可以添加动画效果
})
}
})
}
}
🎓 Step 2: 核心概念讲解
2.1 GridRow(行容器)
GridRow({
columns: 3, // 定义总共3列
gutter: 12, // 列之间的间距(单位:vp)
breakpoints: { // 响应式断点(可选)
value: ['320vp', '520vp', '840vp'],
reference: BreakpointsReference.WindowWidth
}
})
参数说明:
columns:总列数,这里设置为 3 列gutter:列之间的间距breakpoints:断点配置,用于不同屏幕尺寸的适配
2.2 GridCol(列容器)
GridCol({ span: 1 }) {
this.buildCard(card)
}
参数说明:
span: 1:占据 1 列(如果设置span: 2则占据 2 列)
2.3 响应式断点系统(进阶)
breakpoints: {
value: ['320vp', '520vp', '840vp'],
reference: BreakpointsReference.WindowWidth
}
这意味着:
- 0-320vp:超小屏(可以设置 1 列)
- 320-520vp:小屏(可以设置 2 列)
- 520-840vp:中屏(可以设置 3 列)
- 840vp+:大屏(可以设置 4+ 列)
📐 Step 3: 进阶配置(可选)
3.1 根据屏幕尺寸调整列数
如果想让布局更智能,可以添加断点响应:
@State currentColumns: number = 3
GridRow({
columns: this.currentColumns, // 动态列数
gutter: 12
}) {
// ...
}
然后监听窗口变化:
aboutToAppear() {
this.initCards()
this.updateColumnsByScreenSize()
}
private updateColumnsByScreenSize(): void {
const screenWidth = display.getDefaultDisplaySync().width
if (screenWidth < 320) {
this.currentColumns = 1
} else if (screenWidth < 520) {
this.currentColumns = 2
} else if (screenWidth < 840) {
this.currentColumns = 3
} else {
this.currentColumns = 4
}
}
3.2 使用不同的卡片跨度
让某些卡片占据 2 列:
ForEach(this.cards, (card: FunctionCard, index: number) => {
GridCol({
span: index === 0 ? 2 : 1 // 第一个卡片占2列
}) {
this.buildCard(card)
}
}, (card: FunctionCard) => card.id)
3.3 添加加载动画
@State isLoading: boolean = true
aboutToAppear() {
this.initCards()
// 模拟加载
setTimeout(() => {
this.isLoading = false
}, 500)
}
// 在 build 中添加加载状态
if (this.isLoading) {
LoadingProgress()
.width(50)
.height(50)
} else {
this.buildCardGrid()
}
🔧 Step 4: 注册路由并运行
4.1 添加路由配置
编辑 entry/src/main/resources/base/profile/main_pages.json:
{
"src": [
"pages/Index",
"pages/ResponsiveGridDemo"
]
}
4.2 从主页跳转
在你的主页添加跳转按钮:
Button('查看响应式布局示例')
.onClick(() => {
router.pushUrl({
url: 'pages/ResponsiveGridDemo'
})
})
4.3 运行应用
- 连接设备或启动模拟器
- 点击 DevEco Studio 的运行按钮
- 点击主页的按钮进入示例页面
🎨 Step 5: 自定义主题和样式
5.1 修改卡片样式
在 buildCard() 方法中修改:
.backgroundColor(this.isDarkMode ? '#1E1E1E' : '#FFFFFF')
.borderRadius(16) // 更大的圆角
.border({
width: 2, // 更宽的边框
color: card.color // 使用卡片的主题色
})
5.2 添加渐变背景
.linearGradient({
angle: 135,
colors: [[card.color, 0.0], [card.color, 0.3]]
})
5.3 自定义间距
GridRow({
columns: 3,
gutter: { x: 16, y: 16 } // 横向和纵向不同间距
})
📊 响应式布局最佳实践
✅ DO(推荐做法)
- 使用相对单位:
vp、%而非固定的px - 合理设置 gutter:保持视觉舒适的间距(8-16vp)
- 限制列数:手机端 2-4 列,平板端 4-6 列
- 统一卡片高度:使用固定或最小高度保持整齐
- 提供加载状态:数据加载时显示骨架屏或加载动画
❌ DON’T(避免做法)
- 避免过多列数:手机端超过 4 列会显得拥挤
- 避免不一致的间距:保持统一的 gutter 值
- 避免固定像素宽度:会在不同设备上显示异常
- 避免过度动画:影响性能和用户体验
🐛 常见问题解决
Q1: 卡片显示不全/溢出
解决方案:确保使用 .width('100%') 而不是固定宽度
GridCol({ span: 1 }) {
this.buildCard(card)
}
.width('100%') // 添加这一行
Q2: 间距不均匀
解决方案:使用 gutter 而不是手动设置 margin
// ❌ 错误
GridRow({ columns: 3 }) {
// 不要手动添加 margin
}
// ✅ 正确
GridRow({
columns: 3,
gutter: 12 // 使用 gutter 统一管理间距
})
Q3: 深色模式不生效
解决方案:确保正确监听主题状态
@StorageProp('app_is_dark_mode') isDarkMode: boolean = false
并在首页添加主题切换逻辑(参考项目中的 ThemeModel)。
Q4: 点击事件不响应
解决方案:检查是否添加了 .onClick() 处理器
.onClick(() => {
promptAction.showToast({
message: `点击了 ${card.title}`,
duration: 2000
})
})
🎯 扩展练习
练习 1: 添加搜索功能
在卡片网格上方添加搜索框,实现卡片过滤:
@State searchText: string = ''
@State filteredCards: FunctionCard[] = []
// 搜索框
TextInput({ placeholder: '搜索功能...' })
.onChange((value: string) => {
this.searchText = value
this.filterCards()
})
private filterCards(): void {
if (this.searchText.trim() === '') {
this.filteredCards = this.cards
} else {
this.filteredCards = this.cards.filter(card =>
card.title.includes(this.searchText)
)
}
}
练习 2: 添加分类标签
给卡片添加分类,支持按分类筛选:
interface FunctionCard {
// ... 其他属性
category: 'tools' | 'settings' | 'info'
}
// 添加分类过滤按钮
Row({ space: 8 }) {
Button('工具').onClick(() => this.filterByCategory('tools'))
Button('设置').onClick(() => this.filterByCategory('settings'))
Button('信息').onClick(() => this.filterByCategory('info'))
}
练习 3: 添加骨架屏加载
在数据加载时显示骨架屏:
@Builder
buildSkeletonCard() {
Column() {
// 图标骨架
Row()
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor('#E0E0E0')
.margin({ bottom: 12 })
// 标题骨架
Row()
.width('80%')
.height(16)
.borderRadius(4)
.backgroundColor('#E0E0E0')
.margin({ bottom: 8 })
// 描述骨架
Row()
.width('60%')
.height(12)
.borderRadius(4)
.backgroundColor('#E0E0E0')
}
.width('100%')
.padding(16)
}
📖 总结
通过本教程,你学会了:
✅ GridRow/GridCol 基础用法
✅ 响应式网格布局实现
✅ 深色/浅色主题适配
✅ 卡片组件设计
✅ 交互效果实现
关键要点
- GridRow 定义网格容器和列数
- GridCol 定义每个元素占据的列数
- gutter 控制元素间距
- breakpoints 实现响应式断点
- @StorageProp 实现主题状态监听
🔗 相关资源
- HarmonyOS 官方文档 - 网格布局
- ArkUI 响应式布局指南
- 本项目源码:
entry/src/main/ets/components/home/QuickAccessGrid.ets
💡 下一步
学习更多响应式布局技巧:
- 媒体查询:根据屏幕尺寸动态调整布局
- 栅格系统:使用 Row/Column 配合百分比布局
- Swiper 组件:实现可滑动的卡片列表
- List 组件:高性能的长列表展示
🎉 恭喜!你已经掌握了 HarmonyOS 响应式布局的核心技能!
加入班级一起学习 https://developer.huawei.com/consumer/cn/training/classDetail/fd34ff9286174e848d34cde7f512ce22?type=1%3Fha_source%3Dhmosclass&ha_sourceId=89000248
更多推荐
所有评论(0)