第5.4篇:自适应布局 API 实战------adaptiveLayout 模块
难度 :⭐⭐ 进阶
前置知识 :5.1 鸿蒙断点系统原理
涉及源文件 :
features/adaptiveLayout/src/main/ets/pages/AdaptiveLayout.ets、features/adaptiveLayout/src/main/ets/view/SliderComponent.ets
概述
在"画伴梦工厂"的多设备适配体系中,除了基于断点系统的响应式布局,还需要一种更灵活的能力------自适应布局(Adaptive Layout) 。与响应式布局依赖屏幕宽度断点不同,自适应布局的核心思想是让容器内部的子元素根据容器自身的尺寸变化自动调整排列方式,无需依赖外部屏幕宽度。
HarmonyOS 的 ArkUI 框架提供了 flexShrink、flexGrow、Blank() 等弹性布局能力,配合 Slider 组件实现容器宽度实时可控。本文将基于 adaptiveLayout 模块的完整代码,深入讲解自适应布局 API 的实战用法。
一、adaptiveLayout 模块结构
整个自适应布局模块位于 features/adaptiveLayout 目录下,采用典型的多文件模块结构:
features/adaptiveLayout/
├── Index.ets # 模块入口,导出 adaptiveLayout 构建函数
├── src/main/ets/
│ ├── pages/AdaptiveLayout.ets # 主页面:自适应布局容器与子项
│ ├── view/SliderComponent.ets # Slider 滑块组件,控制容器宽度
│ └── constants/CommonConstants.ets # 共享常量定义
职责划分非常清晰:
| 文件 | 职责 |
|---|---|
AdaptiveLayout.ets |
定义自适应布局的容器(Column/Row)及子项样式,实现弹性排列 |
SliderComponent.ets |
提供 Slider 滑块,用户拖动滑块改变容器宽度百分比 |
CommonConstants.ets |
存放布局常量(flexShrink 值、Slider 范围、尺寸常量等) |
Index.ets |
导出 adaptiveLayout() 构建函数,供外部模块引用 |
这种分层使得布局逻辑与 UI 控制分离------AdaptiveLayoutItem 只关心子元素如何排列,SliderComponent 只负责提供交互控制,两者通过 @LocalStorageLink 共享一个状态变量。
二、@LocalStorageLink 跨组件状态共享
模块中最为关键的设计是使用 @LocalStorageLink 装饰器实现跨组件状态共享:
typescript
// AdaptiveLayout.ets --- 布局组件
@Component
struct AdaptiveLayoutItem {
@LocalStorageLink('containerWidth') containerWidth: number = CONTAINER_WIDTH;
// ...
}
// SliderComponent.ets --- 滑块组件
@Component
export struct SliderComponent {
@LocalStorageLink('containerWidth') containerWidth: number = CONTAINER_WIDTH;
// ...
}
@LocalStorageLink 是 ArkUI 中用于跨组件共享状态的装饰器,它的特性如下:
| 特性 | 说明 |
|---|---|
| 存储位置 | 数据存储在 LocalStorage 中,是页面级全局存储 |
| 双向绑定 | 任何一个组件修改值,所有绑定同一 key 的组件都会同步更新 |
| 类型安全 | 泛型约束,编译阶段即可发现类型不匹配 |
| 生命周期 | 随页面生命周期自动管理,无需手动注册/注销 |
在本文的实现中,containerWidth 作为共享状态变量,持有容器宽度的百分比值(范围 55~90)。当用户在 SliderComponent 中拖动滑块时,onChange 回调立即更新 containerWidth;AdaptiveLayoutItem 中的 containerWidth 随之变化,容器宽度自动刷新。
与
@StorageLink的区别:@LocalStorageLink作用于页面级LocalStorage,而@StorageLink作用于应用级AppStorage。对于单个页面内的自适应布局,使用@LocalStorageLink更为合适------页面销毁后状态自动释放,不会污染全局命名空间。
三、Slider 控制容器宽度(55% ~ 90%)
SliderComponent 是整个交互的入口,它提供了一个可拖动的滑块,让用户动态调整容器的宽度百分比:
typescript
// SliderComponent.ets
import { commonConstants } from '../constants/CommonConstants';
@Component
export struct SliderComponent {
@LocalStorageLink('containerWidth') containerWidth: number = 55;
build() {
Slider({
value: this.containerWidth,
min: commonConstants.sliderMin, // 55
max: commonConstants.sliderMax // 90
})
.width($r('app.float.slider_width')) // 300vp
.onChange((value: number) => {
this.containerWidth = value;
})
}
}
参数详解
| 参数 | 值 | 含义 |
|---|---|---|
value |
this.containerWidth |
当前滑块值,由 @LocalStorageLink 驱动 |
min |
55 |
最小宽度百分比,对应容器占父元素的 55% |
max |
90 |
最大宽度百分比,对应容器占父元素的 90% |
| 步长(step) | 默认 1 | 每次拖动递增/递减 1 个百分点 |
onChange 响应机制
Slider 组件的 onChange 回调在用户拖动滑块时持续触发。这里直接将 value 赋值给 this.containerWidth:
typescript
.onChange((value: number) => {
this.containerWidth = value;
})
由于 @LocalStorageLink 的双向绑定特性,这一赋值行为会触发两件事:
LocalStorage中containerWidth的值被更新- 所有绑定同一 key 的组件(包括
AdaptiveLayoutItem)收到变更通知,触发 UI 重渲染
整个过程是单向数据流的典型实践:用户交互 → 状态变更 → UI 刷新,链路清晰、可预测。
四、flexShrink 弹性属性
adaptiveLayout 模块的核心弹性布局能力来自 flexShrink 属性。在 AdaptiveLayoutItem 的外层容器上:
typescript
// AdaptiveLayout.ets
Column() {
// 内层容器(包裹 Row 子项)
Column() {
Row() {
// left child + Blank() + right child
}
.width('' + this.containerWidth + commonConstants.percentSign) // 动态宽度
}
.width(commonConstants.columnWidth) // 100%
.flexShrink(commonConstants.columnFlexShrink) // flexShrink: 1
.height(commonConstants.columnHeight) // 100%
.justifyContent(FlexAlign.Center)
// ...
}
flexShrink 的语义
flexShrink 是 CSS Flexbox 规范在 ArkUI 中的实现,用于定义弹性容器在空间不足时的收缩比例:
| 值 | 行为 |
|---|---|
0 |
不收缩,保持原始尺寸 |
1 |
按比例收缩,适应剩余空间 |
>1 |
相对其他元素以更大的比例收缩 |
在本模块中,columnFlexShrink: 1 意味着当父容器空间不足时,该列会按比例收缩以适配可用空间,不会溢出或破坏布局。
配合动态宽度使用
关键的一行代码是将 containerWidth 拼接到 width 属性中:
typescript
.width('' + this.containerWidth + commonConstants.percentSign)
// 等价于 .width('55%') 到 .width('90%') 之间的动态值
'' + this.containerWidth + '%' 这种字符串拼接方式,在 ArkUI 中完全合法。width 属性接受百分比字符串,当 containerWidth 从 55 变化到 90 时,实际渲染的容器宽度从父元素的 55% 变化到 90%。
这种动态宽度拼接的写法简洁直观,特别适合需要实时响应数值变化的场景。与之对比,如果使用 $r('app.float.xxx') 资源引用方式,就无法实现运行时动态变化。
五、Blank() 弹性占位空间
在 Row 子项中,Blank() 组件是实现子元素自适应排列的关键:
typescript
Row() {
Row() // left child
.width(96vp)
.height(20vp)
.backgroundColor($r('sys.color.ohos_id_color_component_normal'))
.borderRadius(2vp)
Blank() // ← 弹性占位空间
Row() // right child
.width(36vp)
.height(20vp)
.backgroundColor($r('sys.color.ohos_id_color_component_normal'))
.borderRadius(12vp)
}
Blank() 的工作原理
Blank() 组件在 ArkUI 中充当弹性空白占位符,它的行为特点如下:
| 场景 | 行为 |
|---|---|
| 容器有剩余空间 | Blank() 自动填充所有剩余空间 |
| 容器空间不足 | Blank() 优先被压缩,保护两侧子元素的尺寸 |
多个 Blank() |
按比例均分剩余空间 |
在本模块中,当用户通过 Slider 将容器宽度从 90% 缩小到 55% 时:
- 容器本身变窄 → 外层 Column 的宽度缩减
- Row 的可用空间减少 → 左右两个子 Row 的固定尺寸(96vp + 36vp)保持不变
- Blank() 自动收缩 → 中间的弹性空白区域逐步被压缩
- 极限情况 → 当容器窄到 Blank() 为 0 时,左右子元素紧密排列
这种机制实现了容器宽度变化时子元素自动自适应排列,无需手动调整任何一个子元素的位置或尺寸。
六、动态宽度字符串拼接
在 ArkUI 中,组件的 width 属性支持多种类型的输入值:
typescript
// 方式一:数字 + vp 单位
.width(100)
// 方式二:百分比字符串
.width('50%')
// 方式三:资源引用
.width($r('app.float.my_width'))
在自适应布局场景中,需要运行时动态计算百分比,因此采用方式二的变体------模板字符串拼接:
typescript
.width('' + this.containerWidth + commonConstants.percentSign)
commonConstants.percentSign 定义为 '%' 常量,这种写法相比直接写 '55%' 或 '90%' 更加灵活:
containerWidth由 Slider 驱动持续变化- 字符串拼接结果始终是有效的百分比值
- 常量
percentSign保持语义清晰,便于后续维护
实际运行时,宽度会呈现如下变化:
| Slider 值 | 实际 width 属性 | 渲染效果 |
|---|---|---|
| 55 | '55%' |
容器占父元素宽度的 55%,Blank() 空间最小 |
| 72 | '72%' |
容器占父元素宽度的 72%,Blank() 空间适中 |
| 90 | '90%' |
容器占父元素宽度的 90%,Blank() 空间最大 |
七、子元素样式配置
左右两个子 Row 使用了不同的 borderRadius,产生视觉上的差异化:
typescript
Row() // 左侧矩形
.width($r('app.float.flexible_left_item_width')) // 96vp
.height($r('app.float.flexible_left_item_height')) // 20vp
.borderRadius($r('app.float.flexible_left_item_border_radius')) // 2vp
Blank()
Row() // 右侧胶囊
.width($r('app.float.flexible_right_item_width')) // 36vp
.height($r('app.float.flexible_right_item_height')) // 20vp
.borderRadius($r('app.float.flexible_right_item_border_radius')) // 12vp
两者的尺寸和圆角差异,使得自适应效果在视觉上更容易被观察------当容器宽度变化时,左右两个形状不同的元素之间的距离(即 Blank() 区域)会同步变化,让弹性效果一目了然。
所有尺寸值均通过 $r() 引用 float.json 资源文件中的预定义值,符合鸿蒙资源管理最佳实践。
八、完整页面组装
adaptiveLayout 构建函数将布局组件挂载到全屏容器中:
typescript
@Builder
export function adaptiveLayout() {
Column() {
AdaptiveLayoutItem()
}
.height(commonConstants.height) // '100%'
.justifyContent(FlexAlign.Center)
}
使用 @Builder 导出构建函数,而非直接导出组件,是模块化设计的常见模式:
- 外部模块通过
import { adaptiveLayout } from '...'直接调用 Index.ets中统一导出,将内部组件封装为纯函数- 调用者无需了解
AdaptiveLayoutItem内部实现
typescript
// Index.ets
export { adaptiveLayout } from './src/main/ets/pages/AdaptiveLayout';
九、控制与内容分离的设计模式
整个 adaptiveLayout 模块体现了控制与内容分离的设计思想:
┌──────────────────────────────────────────────────┐
│ AdaptiveLayoutItem │
│ ┌────────────────────────────────────────────┐ │
│ │ 内容区域(Content) │ │
│ │ ┌──────┐ ┌──────────────────┐ ┌──────┐ │ │
│ │ │ Left │ │ Blank() 弹性区 │ │ Right│ │ │
│ │ └──────┘ └──────────────────┘ └──────┘ │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ 控制区域(Control) │ │
│ │ ┌──────────────┐ │ │
│ │ │ Slider 滑块 │ │ │
│ │ └──────────────┘ │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
这种分离的优势:
| 优势 | 说明 |
|---|---|
| 职责单一 | 内容区域只关心布局排列,控制区域只负责交互输入 |
| 可替换性 | 可以替换 Slider 为其他控制方式(如按钮组、输入框)而不影响布局逻辑 |
| 可测试性 | 内容区域可以独立测试,只需给定不同的 containerWidth 值 |
| 状态集中 | 唯一的共享状态 containerWidth 通过 @LocalStorageLink 在两者间传递 |
这也是鸿蒙开发中推荐的状态管理实践------将 UI 交互产生的变化集中到一个状态变量,再由该变量驱动视图更新,而非让多个组件各自维护零散的状态。
十、无断点的实时响应
与响应式布局(Responsive Layout)依赖屏幕宽度断点不同,自适应布局的实时响应具有以下特点:
响应式 vs 自适应
| 维度 | 响应式布局(BreakPointType) | 自适应布局(Flexible) |
|---|---|---|
| 触发条件 | 屏幕宽度跨越断点(如 600px/840px) | 容器宽度持续变化 |
| 变化粒度 | 离散(sm/md/lg/xl 四档) | 连续(55% ~ 90% 任意值) |
| 布局方式 | 切换不同的布局模板 | 弹性伸缩同一套布局 |
| 控制手段 | 自动检测屏幕宽度 | Slider 用户手动控制 |
| 典型场景 | 手机/平板/折叠屏适配 | 分屏模式、可调面板、演示模式 |
在本模块中,用户可以实时拖拽 Slider,观察到容器宽度在 55% ~ 90% 之间连续变化,子元素间距(Blank() 区域)平滑伸缩。这种体验无法通过断点切换实现------断点只能提供有限的几种布局快照,而自适应布局提供了无缝的连续过渡。
实际应用场景
在"画伴梦工厂"的后续版本中,这种自适应能力可用于:
- 画布工具栏:用户拖动调整工具栏宽度,子按钮自动紧凑或舒展排列
- 图片编辑面板:右侧参数面板宽度可调,内部控件自适应排列
- 分屏写作模式:左侧画布与右侧对话区通过 Slider 调整比例
十一、参数映射表
为方便理解,将 adaptiveLayout 模块中所有关键参数汇总如下:
| 参数 | 来源 | 值 | 作用 |
|---|---|---|---|
containerWidth |
@LocalStorageLink |
55~90 | 容器宽度百分比 |
columnFlexShrink |
CommonConstants |
1 | 弹性收缩因子 |
sliderMin |
CommonConstants |
55 | Slider 最小值 |
sliderMax |
CommonConstants |
90 | Slider 最大值 |
sliderMargin |
CommonConstants |
24 | Slider 底部边距 |
flexible_left_item_width |
float.json |
96vp | 左侧子元素宽度 |
flexible_right_item_width |
float.json |
36vp | 右侧子元素宽度 |
flexible_left_item_border_radius |
float.json |
2vp | 左侧子元素圆角 |
flexible_right_item_border_radius |
float.json |
12vp | 右侧子元素圆角 |
flexible_item_border_radius |
float.json |
24vp | 容器圆角 |
slider_width |
float.json |
300vp | Slider 组件宽度 |
总结
本文通过 adaptiveLayout 模块的完整代码,系统介绍了 HarmonyOS 自适应布局 API 的实战应用:
| 知识点 | 实现方式 |
|---|---|
| 跨组件状态共享 | @LocalStorageLink 装饰器绑定同一 key |
| 用户控制宽度 | Slider 组件 + onChange 回调实时更新 |
| 弹性布局核心 | flexShrink: 1 按比例收缩容器 |
| 弹性占位 | Blank() 组件自动填充/压缩 |
| 动态宽度 | 字符串拼接 '' + containerWidth + '%' |
| 控制与内容分离 | Slider 与布局容器分属不同组件 |
| 连续响应 | 55% ~ 90% 无级变化,无需断点 |
adaptiveLayout 模块虽然代码量不大,却展示了鸿蒙自适应布局的完整范式------从状态共享到弹性排列,从用户交互到 UI 刷新,形成了一个闭环的数据流。
下一节 :第 5.5 篇将进入 responsiveLayout 模块,结合 Tabs 与 BreakPointType 实现更复杂的响应式布局方案。
参考源码
本文所有代码均来自项目文件:
features/adaptiveLayout/src/main/ets/pages/AdaptiveLayout.ets--- 自适应布局主页面,包含AdaptiveLayoutItem组件和adaptiveLayout()构建函数features/adaptiveLayout/src/main/ets/view/SliderComponent.ets--- Slider 滑块组件,控制容器宽度features/adaptiveLayout/src/main/ets/constants/CommonConstants.ets--- 布局常量定义features/adaptiveLayout/src/main/resources/base/element/float.json--- 浮点数资源文件,定义子元素尺寸和圆角