第5.5篇:响应式布局实战------responsiveLayout 模块
系列 :HarmonyOS 从入门到实践 · 画伴梦工厂实战
难度 :⭐⭐⭐ 高级
前置知识 :5.2 BreakPointType 泛型工具、ArkUI 基础组件(Tabs、Grid、SymbolGlyph)
涉及源文件 :
features/responsiveLayout/src/main/ets/pages/ResponsiveLayout.ets、features/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 中注销 |
aboutToAppear 和 aboutToDisappear 的生命周期管理遵循对称原则:
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))
这里的关键点是:
BreakPointType<Padding>:类型参数指定为Padding接口,确保每个断点的值都是包含top和left属性的对象。$r('app.float.xxx'):所有尺寸值都通过资源引用管理,而不是硬编码。这符合资源归一化的最佳实践,后续调整只需修改资源文件。- 断点差异化:每个断点(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 宽高比)和一行标题文字。通过 columnsGap 和 rowsGap 控制网格间距,确保内容在不同尺寸下都有舒适的视觉密度。
九、多种响应式技术在同一页面的融合
responsiveLayout 模块最大的学习价值在于------它将多种响应式技术无缝融合在同一个页面中,形成了一个完整的响应式布局解决方案。我们来梳理一下这个页面中用到的所有响应式技术:
9.1 断点系统(BreakpointSystem)
typescript
private readonly breakpointSystem: BreakpointSystem = new BreakpointSystem('mainBreakpoint');
aboutToAppear() {
this.breakpointSystem.register(this.getUIContext());
}
aboutToDisappear() {
this.breakpointSystem.unregister();
}
断点系统是整个响应式布局的"发动机",它负责监听屏幕尺寸变化,并更新全局断点状态。
9.2 @StorageLink 跨组件状态同步
typescript
@StorageLink('mainBreakpoint') currentBreakpoint: string = commonConstants.breakpointsInitializeName;
TabIndex 通过 @StorageLink 订阅断点变化;GridComponent 则通过 @StorageProp 订阅同一变量:
typescript
@StorageProp('mainBreakpoint') currentBreakpoint: string = commonConstants.breakpointsInitializeName;
这里体现了 @StorageLink 和 @StorageProp 的配合使用:前者在拥有写入权限的主组件中使用,后者在只读的子组件中使用。
9.3 BreakPointType 泛型
从 barPosition 到 vertical,从 barWidth 到 barHeight,从 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 的响应式策略:
- sm → md:布局形态一致(底部水平 Tab),但 Grid 列数从 2 列增加到 3 列,利用增加的屏幕宽度展示更多内容。
- md → lg:布局形态发生根本性变化(底部水平 → 左侧垂直),Tab 栏从全宽变固定宽度,从固定高度变比例高度,Grid 列数增加到 4 列。
- lg → xl:布局形态基本一致,仅在部分细节值上微调。
这种分阶段的响应式策略,既保证了小屏幕上的可用性,又充分利用了大屏幕的空间优势,是 HarmonyOS 响应式设计的典型实践。
总结
responsiveLayout 模块虽然页面结构不算复杂,但它在单个页面中集中展示了 HarmonyOS 响应式布局的几乎所有核心技术点:
| 技术点 | 应用位置 | 关键代码 |
|---|---|---|
| BreakpointSystem | TabIndex 生命周期 | register(getUIContext()) / unregister() |
| @StorageLink/@StorageProp | 跨组件断点同步 | @StorageLink('mainBreakpoint') |
| BreakPointType<BarPosition> | Tab 栏位置 | sm/md: End → lg/xl: Start |
| BreakPointType<boolean> | Tab 垂直/水平 | sm/md: false → lg/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--- 所有断点常量和模板值定义