A hand-drawn doodle illustration on pure white pap

前言

折叠屏展开是平板、合上是手机——这种场景你写两套界面吗?显然不行。HarmonyOS7 提供了完整的响应式布局方案:断点系统 + GridRow/GridCol + 媒体查询,三件套一配,一套代码跑遍所有设备。今天就来拆解这套方案。

我之前做的一个 App,手机上跑得挺好,上了平板就被吐槽"全是空白浪费屏幕"。后来用响应式布局改造,手机上一列、平板上两列、大屏上三列,一套代码搞定。整个改造过程其实没多复杂,关键是理解断点系统。

多设备适配的挑战

先看下不同设备的屏幕差异有多大:

| 设备类型 | 典型宽度 | 布局特点 |

A hand-drawn doodle illustration on pure white pap

|----------|----------|----------|
| 手机 | 360-420vp | 单列,纵向滚动 |
| 折叠屏(合) | 360-400vp | 同手机 |
| 折叠屏(开) | 700-800vp | 双列,侧边栏 |
| 平板 | 800-1280vp | 多列,复杂布局 |
| 智慧屏 | 1280vp+ | 多列+侧边栏 |

核心问题:同样的内容,不同宽度要呈现不同的布局。你不能写死列数,得让布局根据宽度自动调整。

断点系统

HarmonyOS7 定义了 4 个标准断点,这是响应式布局的基础:

断点 宽度范围 典型设备 推荐列数
xs [0, 320vp) 穿戴设备 1 列
sm [320, 600vp) 手机 1-2 列
md [600, 840vp) 折叠屏展开/小平板 2-3 列
lg [840vp, +∞) 平板/智慧屏 3-4 列

系统会根据当前窗口宽度自动判断落在哪个断点区间。你只需要针对不同断点定义不同的布局参数。

断点判断逻辑
function getCurrentBreakpoint(width: number): string {
  if (width < 320) return 'xs'
  if (width < 600) return 'sm'
  if (width < 840) return 'md'
  return 'lg'
}

实际开发中不用手写这个函数,GridRow 组件内置了断点系统。

GridRow/GridCol 响应式

这是 HarmonyOS7 响应式布局的核心组件。GridRow 是栅格容器,GridCol 是栅格子组件——两者配合,自动根据断点调整列数。

@Entry
@Component
struct ResponsiveGridDemo {
  @State currentBp: string = 'sm'
  private items: string[] = ['新闻', '视频', '音乐', '游戏', '购物', '旅行']

  build() {
    Column() {
      Text(`当前断点: ${this.currentBp}`)
        .fontSize(14)
        .margin({ bottom: 12 })

      GridRow({
        columns: { xs: 2, sm: 2, md: 3, lg: 4 },
        gutter: { x: 12, y: 16 },
        breakpoints: {
          value: ['320vp', '600vp', '840vp'],
          reference: BreakpointsReference.WindowSize
        }
      }) {
        ForEach(this.items, (item: string) => {
          GridCol({ span: { xs: 2, sm: 1, md: 1, lg: 1 } }) {
            Column() {
              Text(item)
                .fontSize(16)
                .fontColor(Color.White)
            }
            .width('100%')
            .height(80)
            .backgroundColor('#007DFF')
            .borderRadius(8)
            .justifyContent(FlexAlign.Center)
          }
        })
      }
      .width('100%')
      .onBreakpointChange((breakpoint: string) => {
        this.currentBp = breakpoint
      })
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

A hand-drawn doodle illustration on pure white pap

逐行讲解

  • columns: { xs: 2, sm: 2, md: 3, lg: 4 } ——定义不同断点下的栅格列数。手机 2 列,平板 3 列,大屏 4 列
  • gutter 控制行列间距
  • breakpoints 定义断点阈值,reference 指定参考窗口大小
  • GridColspan 定义子组件在不同断点下占据的列数
  • onBreakpointChange 在断点变化时回调,可以用来做额外的 UI 调整

效果:手机上 2 列展示,折叠屏展开自动变 3 列,平板上 4 列。代码就这么多,不用写任何条件判断。

媒体查询

GridRow/GridCol 能处理栅格布局的响应式,但有些场景需要更细粒度的控制——比如某个组件只在特定宽度下显示。这时候就要用媒体查询了。

import { mediaQuery } from '@kit.ArkUI'

@Entry
@Component
struct MediaQueryDemo {
  @State isWide: boolean = false
  private listener: mediaQuery.MediaQueryListener | null = null

  aboutToAppear() {
    this.listener = this.getUIContext().getMediaQuery()
      .matchMediaSync('(width>=600vp)')
    this.listener.on('change', (result: mediaQuery.MediaQueryResult) => {
      this.isWide = result.matches as boolean
    })
  }

  aboutToDisappear() {
    if (this.listener) {
      this.listener.off('change')
    }
  }

  build() {
    Row() {
      if (this.isWide) {
        Column() {
          Text('侧边栏')
            .fontSize(16)
        }
        .width(240)
        .height('100%')
        .backgroundColor('#F1F3F5')
        .padding(16)
      }

      Column() {
        Text('主内容区')
          .fontSize(20)
      }
      .layoutWeight(1)
      .height('100%')
      .padding(16)
    }
    .width('100%')
    .height('100%')
  }
}

讲解

  • matchMediaSync('(width>=600vp)') 设置查询条件:宽度大于等于 600vp
  • listener.on('change', ...) 注册变化回调,条件匹配状态改变时触发
  • aboutToDisappear 里必须 off 掉监听,否则内存泄漏
  • result.matchestrue 表示当前宽度满足条件

常见查询条件

条件 含义
(width>=600vp) 宽度 ≥ 600vp
(orientation: landscape) 横屏
(orientation: portrait) 竖屏
(height>=800vp) 高度 ≥ 800vp

折叠屏适配

折叠屏是最典型的响应式场景——展开和合上,屏幕宽度可能差一倍。

@Entry
@Component
struct FoldableDemo {
  @State isExpanded: boolean = false
  private listener: mediaQuery.MediaQueryListener | null = null

  aboutToAppear() {
    this.listener = this.getUIContext().getMediaQuery()
      .matchMediaSync('(width>=600vp)')
    this.isExpanded = (this.listener.matches as boolean)
    this.listener.on('change', (result: mediaQuery.MediaQueryResult) => {
      this.isExpanded = result.matches as boolean
    })
  }

  aboutToDisappear() {
    this.listener?.off('change')
  }

  build() {
    if (this.isExpanded) {
      Row() {
        this.SidePanel()
        this.MainContent()
      }
      .width('100%')
      .height('100%')
    } else {
      Column() {
        this.MainContent()
      }
      .width('100%')
      .height('100%')
    }
  }

  @Builder
  SidePanel() {
    Column() {
      Text('列表')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
      List() {
        ForEach(['项目1', '项目2', '项目3'], (item: string) => {
          ListItem() {
            Text(item).fontSize(14).padding(12)
          }
        })
      }
      .layoutWeight(1)
    }
    .width(280)
    .height('100%')
    .backgroundColor('#F5F5F5')
    .padding(16)
  }

  @Builder
  MainContent() {
    Column() {
      Text('详情内容')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
    }
    .layoutWeight(1)
    .height('100%')
    .padding(16)
    .justifyContent(FlexAlign.Center)
  }
}

核心逻辑:折叠屏合上时(< 600vp),只显示主内容区;展开时(≥ 600vp),左侧显示列表、右侧显示详情。这种"列表+详情"的双栏模式在邮件、笔记类 App 里非常常见。

敲黑板:折叠屏的展开/合上会触发 on('change') 回调,但不会重建组件。你的布局变化逻辑必须放在回调里,不能只在 aboutToAppear 里初始化一次。

最佳实践

做响应式布局这些年,我总结了几条经验:

  1. 先定断点再写布局——别上来就写代码,先画不同断点下的线框图
  2. 用 GridRow/GridCol 做栅格——能解决的不要用媒体查询,栅格组件更省代码
  3. 媒体查询管条件逻辑——组件显示/隐藏、布局结构切换用媒体查询
  4. 监听器必须注销——aboutToDisappearoff 掉,不然后果你懂的
  5. 测试三种宽度——至少测 360vp(手机)、700vp(折叠屏展开)、1280vp(平板)
场景 推荐方案 原因
等宽网格排列 GridRow/GridCol 自动响应断点
侧边栏显示/隐藏 媒体查询 需要条件判断
列数自适应 GridRow columns 参数 一行配置搞定
组件显示/隐藏 if 条件 + 媒体查询 灵活控制

写在最后

响应式布局看着复杂,其实就三件事:断点定义 + 栅格布局 + 媒体查询。GridRow/GridCol 负责栅格自适应,媒体查询负责条件逻辑,断点系统是两者的桥梁。

一套代码适配所有设备,这事儿以前在 Web 上靠 CSS 媒体查询做,现在 HarmonyOS7 把这套理念搬到原生组件里了。用起来比 Web 还方便——至少不用写那些 CSS hack。

如果你正在做多设备适配,先把 GridRow 搞明白,大部分场景它就够用了。

Logo

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

更多推荐