这是一个使用鸿蒙技术开发的本地原生记账应用,非常适合大家用来练手。相关源码已上传至 Github,点击此处查看项目。欢迎大家交流、指正,也欢迎提交 PR。

一、引言

在 HarmonyOS 应用开发中,“一次开发,多端部署”是核心设计理念之一。这意味着开发者只需编写一套代码,即可让应用在手机、折叠屏、平板、智慧屏等多种设备上提供一致且优质的用户体验。实现这一目标的关键技术之一便是响应式布局

响应式布局的核心思想是:页面根据不同屏幕尺寸(主要是窗口宽度)自动调整其布局结构。与仅能保证窗口尺寸在小范围内变化时显示正常的自适应布局不同,响应式布局能在窗口尺寸跨度较大时(如从手机宽度变为平板宽度),主动调整页面结构(如从单列变为双列或三列),从而避免图片异常放大、内容稀疏、留白过多等问题,提供更舒适的界面和更好的用户体验。

二、四种核心响应式布局能力

HarmonyOS 为开发者提供了四种强大的响应式布局能力,它们是构建多端适配应用的基石。

能力 简介
断点 将窗口宽度划分为不同的范围(即断点),监听窗口尺寸变化,当断点改变时同步调整页面布局。
媒体查询 支持监听窗口宽度、横竖屏、深浅色、设备类型等多种媒体特征,当媒体特征改变时同步调整页面布局。
栅格布局 栅格组件(GridRow/GridCol)将所在区域划分为有规律的多列,通过调整不同断点下的栅格参数及子组件占据的列数实现不同布局效果。
响应式组件 Navigation、SideBarContainer、Tabs、Swiper、Grid、List 等组件支持通过断点设置实现分栏、挪移、重复等不同展示效果。

HarmonyOS 系统预置了一套标准的断点划分,方便开发者快速适配不同尺寸的设备:

断点标识 尺寸范围 典型设备
xs [0, 320vp) 小屏设备
sm [320vp, 600vp) 普通手机
md [600vp, 840vp) 折叠屏、横屏手机
lg [840vp, +∞) 平板、智慧屏

三、常用相关能力详解

3.1 栅格布局(GridRow/GridCol)

栅格布局是响应式布局中最核心、最常用的能力。它通过 GridRowGridCol 两个组件协同工作:

  • GridRow:作为栅格容器,可以定义总栅格列数(默认12列)和针对不同断点的配置。
  • GridCol:作为栅格列,定义其子元素在不同断点下占用的栅格数。

通过 onBreakpointChange 回调,开发者可以监听断点变化,并执行相应的业务逻辑。

3.2 媒体查询

媒体查询提供了更精细的响应式控制能力。通过 @kit.ArkUI 导入 mediaquery 模块,使用 matchMediaSync 设置查询条件并获取监听句柄,然后注册 on('change') 回调来监听媒体特征的变化。

注意:从 API version 18 起,需通过 this.getUIContext().getMediaQuery().matchMediaSync() 方式调用。

3.3 响应式组件

HarmonyOS 内置了许多支持响应式布局的组件,它们可以极大地简化多端适配的开发工作:

  • Navigation:可根据断点自动在单栏/双栏间切换。
  • Tabs:可将底部导航挪移为侧边导航。
  • List/Grid/WaterFlow:可通过断点控制列数实现重复布局。

四、示例

以下示例综合演示了栅格布局、断点监听和媒体查询三种能力的用法,实现一个功能卡片网格页面,在不同屏幕尺寸下自动调整列数,并响应横竖屏切换:

import { mediaquery } from '@kit.ArkUI';

// 功能卡片数据模型
interface FeatureCard {
  id: string
  title: string
  description: string
  icon: string
}

@Entry
@Component
struct ResponsiveLayoutDemo {
  // 当前断点状态
  @State currentBp: string = 'sm'
  // 是否横屏
  @State isLandscape: boolean = false
  // 媒体查询监听句柄
  private landscapeListener: mediaquery.MediaQueryListener | null = null

  // 功能卡片数据源
  @State cardList: FeatureCard[] = [
    { id: '0', title: '日程管理', description: '高效安排每日计划', icon: '📅' },
    { id: '1', title: '备忘录', description: '随时记录灵感想法', icon: '📝' },
    { id: '2', title: '健康追踪', description: '记录运动与睡眠', icon: '🏃' },
    { id: '3', title: '财务管理', description: '收支明细一目了然', icon: '💰' },
    { id: '4', title: '智能提醒', description: '重要事项不再遗漏', icon: '⏰' },
    { id: '5', title: '文件同步', description: '多端数据实时同步', icon: '📁' }
  ]

  aboutToAppear(): void {
    // 注册横竖屏媒体查询监听
    this.landscapeListener = this.getUIContext().getMediaQuery()
      .matchMediaSync('(orientation: landscape)')
    this.isLandscape = this.landscapeListener.matches
    this.landscapeListener.on('change', (result: mediaquery.MediaQueryResult) => {
      this.isLandscape = result.matches
    })
  }

  aboutToDisappear(): void {
    // 组件销毁时移除媒体查询监听,避免内存泄漏
    if (this.landscapeListener !== null) {
      this.landscapeListener.off('change')
      this.landscapeListener = null
    }
  }

  // 根据断点动态计算卡片标题字号
  getTitleFontSize(): number {
    if (this.currentBp === 'xs' || this.currentBp === 'sm') {
      return 16
    } else if (this.currentBp === 'md') {
      return 18
    } else {
      return 20
    }
  }

  build() {
    Column() {
      // 顶部标题栏
      Row() {
        Text('功能中心')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Blank()
        // 显示当前断点和屏幕方向
        Text(`断点: ${this.currentBp} | ${this.isLandscape ? '横屏' : '竖屏'}`)
          .fontSize(14)
          .fontColor('#999999')
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .justifyContent(FlexAlign.Center)

      // 栅格布局区域:根据断点自动调整列数
      GridRow({
        columns: {
          xs: 2,  // 超小屏:2列
          sm: 2,  // 小屏(手机竖屏):2列
          md: 3,  // 中屏(折叠屏/横屏手机):3列
          lg: 4,  // 大屏(平板):4列
          xl: 6   // 超大屏:6列
        },
        gutter: { x: 12, y: 12 },  // 列间距和行间距均为12vp
        breakpoints: {
          value: ['320vp', '600vp', '840vp'],
          reference: BreakpointsReference.WindowSize  // 以窗口宽度为断点参照
        }
      }) {
        ForEach(this.cardList, (card: FeatureCard) => {
          GridCol({
            span: 1  // 每个卡片占1个栅格列
          }) {
            // 单个功能卡片
            Column() {
              Text(card.icon)
                .fontSize(36)
                .margin({ bottom: 8 })
              Text(card.title)
                .fontSize(this.getTitleFontSize())
                .fontWeight(FontWeight.Medium)
                .fontColor('#333333')
                .margin({ bottom: 4 })
              Text(card.description)
                .fontSize(12)
                .fontColor('#999999')
                .maxLines(2)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .width('100%')
            .height(120)
            .backgroundColor('#FFFFFF')
            .borderRadius(12)
            .padding(12)
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)
            .shadow({ radius: 8, color: 'rgba(0,0,0,0.08)', offsetX: 0, offsetY: 2 })
          }
        }, (card: FeatureCard) => card.id)
      }
      .width('100%')
      .layoutWeight(1)
      .padding(16)
      .onBreakpointChange((breakpoint: string) => {
        // 断点变化时更新当前断点状态,驱动UI刷新
        this.currentBp = breakpoint
      })

      // 底部信息栏:根据断点显示不同提示
      Row() {
        if (this.currentBp === 'sm' || this.currentBp === 'xs') {
          Text('当前为手机模式,建议竖屏使用以获得最佳体验')
            .fontSize(12)
            .fontColor('#666666')
        } else if (this.currentBp === 'md') {
          Text('当前为折叠屏/横屏模式,已自动切换为3列布局')
            .fontSize(12)
            .fontColor('#666666')
        } else {
          Text('当前为大屏模式,已自动切换为4列布局,充分利用屏幕空间')
            .fontSize(12)
            .fontColor('#666666')
        }
      }
      .width('100%')
      .height(40)
      .justifyContent(FlexAlign.Center)
      .backgroundColor('#F5F5F5')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

4.1 运行效果

请添加图片描述

4.2 代码说明

上述示例中核心逻辑如下:

  1. 栅格配置GridRowcolumns 参数为每个断点指定了不同的栅格列数,系统根据当前窗口宽度自动落入对应断点,从而决定一行排列几个卡片。gutter 设置了列间和行间间距,breakpoints 自定义了断点阈值并以窗口宽度为参照。
  2. 断点监听onBreakpointChange 回调在断点跨越时触发,将新断点值存入 @State currentBp,驱动 getTitleFontSize() 和底部信息栏的条件渲染自动刷新。
  3. 媒体查询:在 aboutToAppear 中通过 this.getUIContext().getMediaQuery().matchMediaSync('(orientation: landscape)') 创建横屏监听句柄,注册 on('change') 回调实时更新 isLandscape 状态;在 aboutToDisappear 中调用 off('change') 移除监听,避免内存泄漏。
  4. 列表渲染ForEachkeyGenerator 使用 card.id 保证列表渲染的稳定性,数据模型 FeatureCard 显式声明了所有字段类型,符合 ArkTS 强类型规范。

五、注意事项与最佳实践

  1. 及时移除监听:媒体查询监听必须在组件销毁时移除。未调用 off('change') 会导致回调持续引用已销毁组件,造成内存泄漏。
  2. API 版本适配:从 API version 18 起,mediaquery.matchMediaSync() 已废弃,必须通过 this.getUIContext().getMediaQuery().matchMediaSync() 调用。直接使用旧 API 在高版本设备上可能无法正常工作。
  3. 灵活配置 GridColGridColspan 属性支持按断点分别配置,如 span: { sm: 12, md: 6, lg: 4 },未显式配置的断点会取已配置的更小断点的值进行补全。
  4. 响应式与自适应结合:响应式布局与自适应布局应配合使用。自适应布局(如 layoutWeight、百分比宽度、FlexflexGrow)处理窗口尺寸小幅变化时的连续调整;响应式布局处理跨断点时的结构性变化(如列数切换、分栏/单栏切换)。
  5. 使用 vp 单位:避免使用固定像素(px)定义布局尺寸,优先使用 vp 单位或百分比,配合 Blank 组件填充弹性空间,以适应不同 DPI 和屏幕密度的设备。
  6. 工程架构考量:对于页面复杂度过高的场景,仅靠响应式布局一套代码适配所有设备的成本较高且可读性差,此时建议结合工程级多层架构,为特定设备形态(如手表)单独创建 HAP 包实现差异化布局。

六、总结

响应式布局是实现 HarmonyOS “一次开发,多端部署” 目标的关键技术。通过合理运用断点、媒体查询、栅格布局和响应式组件,开发者可以高效地构建出能够自适应各种屏幕尺寸和形态的应用,为用户提供一致且卓越的跨设备体验。

希望本教程能帮助您快速上手,在实际项目中灵活运用这些强大的布局能力。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐