HarmonyOS 响应式布局实战教程

📚 教程简介

本教程将手把手教你在 HarmonyOS 应用中实现响应式布局。通过一个完整的功能卡片网格页面示例,你将学会:

  • ✅ 使用 GridRowGridCol 实现网格布局
  • ✅ 自动适配不同屏幕尺寸
  • ✅ 支持深色/浅色主题切换
  • ✅ 优雅的卡片设计和交互效果

难度等级:⭐⭐ 入门级
预计时间: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 运行应用

  1. 连接设备或启动模拟器
  2. 点击 DevEco Studio 的运行按钮
  3. 点击主页的按钮进入示例页面

🎨 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(推荐做法)

  1. 使用相对单位vp% 而非固定的 px
  2. 合理设置 gutter:保持视觉舒适的间距(8-16vp)
  3. 限制列数:手机端 2-4 列,平板端 4-6 列
  4. 统一卡片高度:使用固定或最小高度保持整齐
  5. 提供加载状态:数据加载时显示骨架屏或加载动画

❌ DON’T(避免做法)

  1. 避免过多列数:手机端超过 4 列会显得拥挤
  2. 避免不一致的间距:保持统一的 gutter 值
  3. 避免固定像素宽度:会在不同设备上显示异常
  4. 避免过度动画:影响性能和用户体验

🐛 常见问题解决

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 基础用法
响应式网格布局实现
深色/浅色主题适配
卡片组件设计
交互效果实现

关键要点

  1. GridRow 定义网格容器和列数
  2. GridCol 定义每个元素占据的列数
  3. gutter 控制元素间距
  4. breakpoints 实现响应式断点
  5. @StorageProp 实现主题状态监听

🔗 相关资源


💡 下一步

学习更多响应式布局技巧:

  1. 媒体查询:根据屏幕尺寸动态调整布局
  2. 栅格系统:使用 Row/Column 配合百分比布局
  3. Swiper 组件:实现可滑动的卡片列表
  4. List 组件:高性能的长列表展示

🎉 恭喜!你已经掌握了 HarmonyOS 响应式布局的核心技能!
加入班级一起学习 https://developer.huawei.com/consumer/cn/training/classDetail/fd34ff9286174e848d34cde7f512ce22?type=1%3Fha_source%3Dhmosclass&ha_sourceId=89000248

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐