HarmonyOS APP《画伴梦工厂》开发第39篇-响应式布局实战——responsiveLayout模块

第5.5篇:响应式布局实战------responsiveLayout 模块

系列 :HarmonyOS 从入门到实践 · 画伴梦工厂实战

难度 :⭐⭐⭐ 高级

前置知识 :5.2 BreakPointType 泛型工具、ArkUI 基础组件(Tabs、Grid、SymbolGlyph)

涉及源文件features/responsiveLayout/src/main/ets/pages/ResponsiveLayout.etsfeatures/responsiveLayout/src/main/ets/view/GridComponent.ets


一、概述

在前面的文章中,我们分别学习了 BreakPointType 泛型工具的基本原理和断点系统的注册机制。但理论知识终归要落地到实际页面中才能体现其价值。responsiveLayout 模块正是"画伴梦工厂"中一个完整的响应式布局展示案例,它将之前学到的所有响应式技术集中运用到了一个页面中。

这个模块的核心功能是一个响应式 Tab 页面:包含一个可切换的标签栏(Tabs)和内容区域的网格(Grid)。它的"响应式"之处在于------页面的几乎所有视觉属性都根据设备断点(sm / md / lg / xl)动态切换,包括 Tab 栏位置、Tab 排列方向、图标与文字的布局方向、间距与内边距、尺寸宽度、以及 Grid 的列数。

可以说,responsiveLayout 模块是一个"All-in-One"的响应式布局示范页,看完它,你就掌握了 HarmonyOS 响应式设计的大部分实战技巧。


二、TabIndex 组件整体结构

TabIndex 是整个 responsiveLayout 页面的核心组件,定义在 ResponsiveLayout.ets 中。它的基本结构如下:

typescript 复制代码
@Entry
@Component
export struct TabIndex {
  @State currentIndex: number = 0;
  @State tabs: TabBarItem[] = TabsViewModel.getTabData();
  @StorageLink('mainBreakpoint') currentBreakpoint: string = commonConstants.breakpointsInitializeName;
  private readonly breakpointSystem: BreakpointSystem = new BreakpointSystem('mainBreakpoint');
  // ...
}

关键要素解析:

元素 作用
@State currentIndex 当前选中的 Tab 索引,用于高亮 icon 和文字颜色
@State tabs Tab 数据源,通过 TabsViewModel 获取
@StorageLink('mainBreakpoint') 从全局 AppStorage 中读取当前断点值,实现跨组件响应式同步
breakpointSystem 断点系统实例,在 aboutToAppear 中注册,在 aboutToDisappear 中注销

aboutToAppearaboutToDisappear 的生命周期管理遵循对称原则:

typescript 复制代码
aboutToAppear() {
  this.breakpointSystem.register(this.getUIContext());
}

aboutToDisappear() {
  this.breakpointSystem.unregister();
}

当屏幕尺寸变化时,breakpointSystem 自动更新 AppStorage 中的 mainBreakpoint 值,所有通过 @StorageLink@StorageProp 订阅该变量的组件都会收到通知并重新渲染。


三、Tab 栏位置响应式切换

Tab 栏在页面中的位置是响应式布局中最直观的变化之一。在手机(sm / md)上,Tab 栏位于页面底部,而在桌面(lg / xl)上,Tab 栏切换到左侧。

typescript 复制代码
Tabs({
  barPosition: new BreakPointType({
    sm: BarPosition.End,
    md: BarPosition.End,
    lg: BarPosition.Start,
    xl: BarPosition.Start
  })
  .getValue(this.currentBreakpoint)
}) {
  // TabContent...
}

BarPosition.End 表示 Tab 栏在内容区域的末尾(底部),BarPosition.Start 表示在起始位置(左侧)。通过 BreakPointType 泛型,我们为每个断点分别指定不同的 barPosition,从而实现"手机底部导航、桌面侧边导航"的设计模式。

这种模式的用户体验考量:

  • 手机端(sm/md):屏幕宽度有限,底部 Tab 栏符合移动端用户的操作习惯,大拇指可以轻松触及底部图标。
  • 桌面端(lg/xl):屏幕宽裕,侧边 Tab 栏可以释放更多的垂直空间用于展示内容,同时左侧导航也更符合桌面应用的使用直觉。

四、Tab 垂直/水平方向切换

手机的 Tab 栏通常是水平方向(横向排列标签),而桌面侧边栏则需要垂直方向(纵向排列标签)。TabIndex 通过 .vertical() 属性实现这一转换:

typescript 复制代码
.vertical(new BreakPointType({
  sm: false,
  md: false,
  lg: true,
  xl: true
}).getValue(this.currentBreakpoint))

这里的值定义在 CommonConstants 中:

typescript 复制代码
tabSmVertical: false,   // 手机 → 水平
tabMdVertical: false,   // 平板竖屏 → 水平
tabLgVertical: true,    // 桌面 → 垂直
tabXlVertical: true     // 大屏桌面 → 垂直

这一配置与 barPosition 天然协同:当 Tab 栏在底部时,水平排列最为自然;当 Tab 栏在左侧时,垂直排列才是合理的布局。两者结合,实现了完整的 Tab 栏形态转换。


五、Tab 图标与文字布局方向

在 Tab 栏的每个标签项中,图标(SymbolGlyph)和文字(Text)的排列方向也根据断点动态变化。这一逻辑实现在 tabBarBuilder 自定义构建器中:

typescript 复制代码
@Builder tabBarBuilder(tabBar: TabBarItem) {
  Flex({
    direction: new BreakPointType({
      sm: FlexDirection.Column,
      md: FlexDirection.Row,
      lg: FlexDirection.Column,
      xl: FlexDirection.Column
    }).getValue(this.currentBreakpoint),
    justifyContent: FlexAlign.Center,
    alignItems: ItemAlign.Center
  }) {
    SymbolGlyph($r('sys.symbol.person_crop_circle_fill_1'))
      .fontColor(this.currentIndex === tabBar.id ? [...activated] : [...secondary])
    Text(tabBar.name)
      .fontSize($r('app.float.tab_text_font_size'))
      // ...
  }
}

Flex 容器的 direction 属性决定了图标和文字的排列方向:

  • 手机(sm)FlexDirection.Column → 图标在上,文字在下(纵向排列),充分利用窄屏的垂直空间。
  • 平板竖屏(md)FlexDirection.Row → 图标在左,文字在右(横向排列),利用较宽的屏幕展示更多信息。
  • 桌面端(lg/xl)FlexDirection.Column → 图标在上,文字在下,配合垂直 Tab 栏形成统一视觉风格。

这种设计是"响应式细节"的典型体现------不仅仅是调整尺寸,而是根据屏幕空间重新组织信息布局。

SymbolGlyph 图标适配

图标使用 HarmonyOS 5.0 新增的 SymbolGlyph 组件,它支持矢量图标的颜色、大小、粗细等属性的动态调整:

typescript 复制代码
SymbolGlyph($r('sys.symbol.person_crop_circle_fill_1'))
  .fontColor(this.currentIndex === tabBar.id ?
    [$r('sys.color.ohos_id_color_text_primary_activated')] :
    [$r('sys.color.ohos_id_color_text_secondary')])
  .fontSize($r('app.float.tab_img_width'))

currentIndex 等于当前 Tab 的 id 时,图标使用激活态颜色(primary_activated),否则使用次要颜色(secondary)。这一逻辑与 @State currentIndex 联动,实现 Tab 选中状态切换。


六、Padding / Margin 响应式取值

在 Tab 标签中,文字的边距(margin)在不同断点下使用了不同的值。这是通过 BreakPointType<Padding> 泛型实现的:

typescript 复制代码
Text(tabBar.name)
  .fontSize($r('app.float.tab_text_font_size'))
  .margin(new BreakPointType<Padding>({
    sm: { top: $r('app.float.tab_text_sm_top'), left: $r('app.float.tab_text_sm_left') },
    md: { top: $r('app.float.tab_text_md_top'), left: $r('app.float.tab_text_md_left') },
    lg: { top: $r('app.float.tab_text_lg_top'), left: $r('app.float.tab_text_lg_left') },
    xl: { top: $r('app.float.tab_text_xl_top'), left: $r('app.float.tab_text_xl_left') }
  }).getValue(this.currentBreakpoint))

这里的关键点是:

  1. BreakPointType<Padding> :类型参数指定为 Padding 接口,确保每个断点的值都是包含 topleft 属性的对象。
  2. $r('app.float.xxx'):所有尺寸值都通过资源引用管理,而不是硬编码。这符合资源归一化的最佳实践,后续调整只需修改资源文件。
  3. 断点差异化:每个断点(sm/md/lg/xl)都有自己独立的 top 和 left 值,确保文字在不同屏幕尺寸下都有合适的间距。

GridComponent 中,同样可以看到响应式的 margin 和 padding 设置:

typescript 复制代码
.margin({
  left: $r('app.float.grid_margin_left'),
  right: $r('app.float.grid_margin_right')
})
.padding({ bottom: $r('app.float.grid_padding_bottom') })

虽然这里没有使用 BreakPointType 动态切换(因为这些资源本身可能已经根据断点定义了不同的值),但其本质上也是响应式设计的一部分。


七、Tab 栏宽高响应式切换

Tab 栏的宽度和高度在不同断点下采用了完全不同的策略------这是 responsiveLayout 中最具视觉冲击力的响应式变化之一。

宽度切换

typescript 复制代码
.barWidth(new BreakPointType({
  sm: '100%',
  md: '100%',
  lg: '96vp',
  xl: '96vp'
}).getValue(this.currentBreakpoint))
  • 手机端(sm/md)barWidth'100%',Tab 栏占满屏幕宽度。这对应底部水平 Tab 栏的使用场景。
  • 桌面端(lg/xl)barWidth'96vp',Tab 栏固定为 96vp 的窄宽度。这对应左侧垂直侧边栏的使用场景。

高度切换

typescript 复制代码
.barHeight(new BreakPointType({
  sm: '56vp',
  md: '56vp',
  lg: '60%',
  xl: '60%'
}).getValue(this.currentBreakpoint))
  • 手机端(sm/md)barHeight'56vp',固定高度,适合底部导航栏的标准尺寸。
  • 桌面端(lg/xl)barHeight'60%',占容器高度的 60%,为侧边栏的垂直空间提供充足展示区域。

宽度和高度的"角色互换"是响应式布局的精髓所在------同一个属性在不同断点下承载不同的布局职责。当 Tab 栏在底部时,宽度是关键的约束维度;当 Tab 栏在左侧时,高度成为关键的约束维度。


八、Grid 动态列数

Tab 内容区域使用 GridComponent 来展示网格布局。网格的列数根据断点动态变化,这是 responsiveLayout 中另一个核心的响应式技术。

初始化阶段------aboutToAppear

typescript 复制代码
aboutToAppear() {
  // 初始化数据
  for (let i = 0; i < commonConstants.gridSize; i++) {
    this.data.push(i);
  }
  // 根据当前断点设置列模板
  switch (this.currentBreakpoint) {
    case 'sm':
      this.colTemplate = '1fr 1fr';         // 2 列
      break;
    case 'md':
      this.colTemplate = '1fr 1fr 1fr';     // 3 列
      break;
    case 'lg':
      this.colTemplate = '1fr 1fr 1fr 1fr'; // 4 列
      break;
    case 'xl':
      this.colTemplate = '1fr 1fr 1fr 1fr'; // 4 列
      break;
  }
}

Grid 的列模板使用 fr 单位(fraction,分数单位),类似于 CSS Grid 的 fr 单位,表示将可用空间按比例分配给各列。

  • sm(手机)'1fr 1fr' → 2 列网格,每列等宽。手机上屏幕窄,2 列能保证每个格子有足够的可点击区域。
  • md(平板竖屏)'1fr 1fr 1fr' → 3 列网格,更充分地利用增加的屏幕宽度。
  • lg/xl(桌面端)'1fr 1fr 1fr 1fr' → 4 列网格,在大屏幕上展示更多内容。

运行时响应------onAreaChange

Grid 组件还监听了 onAreaChange 事件,在组件区域变化时动态更新列模板:

typescript 复制代码
.onAreaChange(() => {
  this.colTemplate = new BreakPointType({
    sm: commonConstants.smColTemplate,
    md: commonConstants.mdColTemplate,
    lg: commonConstants.lgColTemplate,
    xl: commonConstants.xlColTemplate
  }).getValue(this.currentBreakpoint);
})

这里的值引自 CommonConstants

typescript 复制代码
smColTemplate: '1fr 1fr',
mdColTemplate: '1fr 1fr 1fr',
lgColTemplate: '1fr 1fr 1fr 1fr',
xlColTemplate: '1fr 1fr 1fr 1fr',

onAreaChange 回调在组件尺寸变化时触发,搭配 BreakPointType.getValue(),确保用户在运行过程中调整窗口大小时,Grid 的列数能够实时更新。

完整的 Grid 结构

Grid 的其他属性也值得一提:

typescript 复制代码
Grid() {
  ForEach(this.data, () => {
    GridItem() {
      Column() {
        Row().aspectRatio(16 / 9)  // 16:9 的占位卡片
          .backgroundColor(...)
          .borderRadius(...)
        Row() {
          Text($r('app.string.grid_title_label'))
            .margin({ top: ... })
            .fontSize(...)
        }
      }
    }
  })
}
.columnsGap($r('app.float.grid_column_gap'))
.rowsGap($r('app.float.grid_row_gap'))
.columnsTemplate(this.colTemplate)

每个 GridItem 包含一个 Row 占位卡片(保持 16:9 宽高比)和一行标题文字。通过 columnsGaprowsGap 控制网格间距,确保内容在不同尺寸下都有舒适的视觉密度。


九、多种响应式技术在同一页面的融合

responsiveLayout 模块最大的学习价值在于------它将多种响应式技术无缝融合在同一个页面中,形成了一个完整的响应式布局解决方案。我们来梳理一下这个页面中用到的所有响应式技术:

9.1 断点系统(BreakpointSystem)

typescript 复制代码
private readonly breakpointSystem: BreakpointSystem = new BreakpointSystem('mainBreakpoint');

aboutToAppear() {
  this.breakpointSystem.register(this.getUIContext());
}

aboutToDisappear() {
  this.breakpointSystem.unregister();
}

断点系统是整个响应式布局的"发动机",它负责监听屏幕尺寸变化,并更新全局断点状态。

typescript 复制代码
@StorageLink('mainBreakpoint') currentBreakpoint: string = commonConstants.breakpointsInitializeName;

TabIndex 通过 @StorageLink 订阅断点变化;GridComponent 则通过 @StorageProp 订阅同一变量:

typescript 复制代码
@StorageProp('mainBreakpoint') currentBreakpoint: string = commonConstants.breakpointsInitializeName;

这里体现了 @StorageLink@StorageProp 的配合使用:前者在拥有写入权限的主组件中使用,后者在只读的子组件中使用。

9.3 BreakPointType 泛型

barPositionvertical,从 barWidthbarHeight,从 Flex 方向到 Text 边距,BreakPointType<T> 无处不在,为每一种可配置属性提供断点感知能力。

9.4 资源引用($r)

所有尺寸、颜色、字符串值都通过 $r() 资源引用,而非硬编码值。这为未来的主题切换、多语言适配、不同设备的分辨率适配提供了基础。

9.5 switch 条件列模板

GridComponent 既使用了 aboutToAppear 中的 switch 语句做初始化,又通过 onAreaChange + BreakPointType 做运行时更新,形成了"初始化 + 运行时"的双重响应式保障。


十、完整断点行为汇总表

下面以表格形式汇总 responsiveLayout 模块在所有四个断点下的完整行为:

属性 sm(手机) md(平板竖屏) lg(桌面) xl(大屏桌面)
Tab 栏位置 底部(End) 底部(End) 左侧(Start) 左侧(Start)
Tab 排列方向 水平 水平 垂直 垂直
图标+文字方向 纵向(图标在上) 横向(图标在左) 纵向(图标在上) 纵向(图标在上)
Tab 栏宽度 100%(全宽) 100%(全宽) 96vp(固定窄宽) 96vp(固定窄宽)
Tab 栏高度 56vp(固定) 56vp(固定) 60%(比例) 60%(比例)
Grid 列数 2 列(1fr 1fr) 3 列(1fr 1fr 1fr) 4 列(1fr 1fr 1fr 1fr) 4 列(1fr 1fr 1fr 1fr)
文字 margin sm 独立值 md 独立值 lg 独立值 xl 独立值

从这张表可以清晰地看到 responsiveLayout 的响应式策略:

  1. sm → md:布局形态一致(底部水平 Tab),但 Grid 列数从 2 列增加到 3 列,利用增加的屏幕宽度展示更多内容。
  2. md → lg:布局形态发生根本性变化(底部水平 → 左侧垂直),Tab 栏从全宽变固定宽度,从固定高度变比例高度,Grid 列数增加到 4 列。
  3. lg → xl:布局形态基本一致,仅在部分细节值上微调。

这种分阶段的响应式策略,既保证了小屏幕上的可用性,又充分利用了大屏幕的空间优势,是 HarmonyOS 响应式设计的典型实践。


总结

responsiveLayout 模块虽然页面结构不算复杂,但它在单个页面中集中展示了 HarmonyOS 响应式布局的几乎所有核心技术点:

技术点 应用位置 关键代码
BreakpointSystem TabIndex 生命周期 register(getUIContext()) / unregister()
@StorageLink/@StorageProp 跨组件断点同步 @StorageLink('mainBreakpoint')
BreakPointType<BarPosition> Tab 栏位置 sm/md: Endlg/xl: Start
BreakPointType<boolean> Tab 垂直/水平 sm/md: falselg/xl: true
BreakPointType<FlexDirection> 图标+文字布局方向 Column / Row 切换
BreakPointType<Padding> 响应式边距 每个断点独立 top/left 值
BreakPointType<string> Tab 栏宽/高 '100%''96vp' / '56vp''60%'
Grid columnsTemplate 动态列数 switch 初始化 + onAreaChange 运行时更新
SymbolGlyph 图标选中状态 fontColor 条件切换激活/次要色

通过这个模块,我们可以深刻理解 HarmonyOS 响应式设计的核心理念:不是为每一种设备尺寸分别设计页面,而是通过断点系统按需调整布局属性,实现"一套代码,多屏适配"


参考源码

本文所有代码均来自项目文件:

  • features/responsiveLayout/src/main/ets/pages/ResponsiveLayout.ets --- TabIndex 主组件,包含完整的响应式 Tab 布局实现
  • features/responsiveLayout/src/main/ets/view/GridComponent.ets --- 响应式 Grid 组件,展示动态列模板切换
  • features/responsiveLayout/src/main/ets/viewmodel/TabsViewModel.ets --- Tab 数据源
  • features/responsiveLayout/src/main/ets/viewmodel/TabBarItem.ets --- Tab 项数据模型
  • features/responsiveLayout/src/main/ets/constants/CommonConstants.ets --- 所有断点常量和模板值定义