食材保鲜手册·ArkUI·鸿蒙:双 Tab 与保鲜周期可视化布局的工程实现
本文基于 HarmonyOS Next API 24+ 与 ArkUI 2.x,深入「食材保鲜手册」应用的布局层。我们将从 build() 的顶层 Column 骨架入手,讲透双 Tab 栏的交互实现、保鲜周期与常温保存的视图切换、ForEach 列表渲染、Scroll 容器的滚动策略、Blank 弹性占位、layoutWeight 权重布局等 ArkUI 声明式布局的核心机制,以及这些机制在 360vp 窄屏到 720vp 宽屏上的响应式取舍。


一、build() 的顶层骨架
1.1 单页面应用的布局结构
「食材保鲜手册」是单页面应用——整个应用只有一个 pages/Index.ets 页面,承载所有功能。这种形态适合工具型应用——功能集中、无导航深度、启动即用。
build() 方法的顶层骨架:
build() {
Column() {
// 顶部标题
Row() { ... }
// Tab栏
Row() { ... }
// 概览统计栏
Row() { ... }
// 分类筛选
Scroll() { ... }
// 内容列表
Scroll() { ... }
}
.height('100%')
.width('100%')
.backgroundColor($r('app.color.page_bg'))
}
顶层 Column 是页面的垂直容器,依次堆叠五个区块:标题、Tab、统计、筛选、列表。这个结构是"信息工具型"应用的经典骨架——从上到下是"我是谁(标题)→ 切换什么(Tab)→ 概览什么(统计)→ 筛选什么(筛选)→ 看什么(列表)",符合用户从认知到操作的视觉路径。
.height('100%') 和 .width('100%') 让顶层 Column 占满整个窗口——鸿蒙应用的窗口由 EntryAbility 的 windowStage 创建,Column 占满窗口意味着应用全屏渲染。.backgroundColor($r('app.color.page_bg')) 设置页面背景色,用 $r 引用资源系统而非硬编码,方便深色模式适配。
1.2 Column 的垂直堆叠语义
ArkUI 的 Column 是 Y 轴堆叠容器——子节点从上到下依次排列。本应用顶层 Column 的五个子节点(标题/Tab/统计/筛选/列表)垂直堆叠,前四个是固定高度区块,最后一个 Scroll 用 .layoutWeight(1) 吃掉剩余高度,让列表区滚动。
这种"固定头 + 弹性身"的布局是移动端的惯用法——头部信息固定不滚,身体内容滚动。ArkUI 用 Column + layoutWeight 实现,相当于 CSS Flexbox 的 flex: 1。
如果不用 layoutWeight(1) 而给 Scroll 固定高度(如 .height(500)),在不同屏幕尺寸上会出问题——窄屏 500 太高挤压头部、宽屏 500 太低浪费空间。layoutWeight(1) 让 Scroll 自动吃掉"总高 - 头部高"的剩余,任何屏幕都合理。
1.3 区块间的视觉分层
五个区块的视觉分层靠背景色和边框线实现:
| 区块 | 背景色 | 视觉作用 |
|---|---|---|
| 顶部标题 | header_bg(暖白) |
标题区与 Tab 区的微弱分层 |
| Tab 栏 | header_bg(暖白) + 底部 1vp 边框 |
与下方内容区的强分层 |
| 概览统计 | page_bg(米黄) |
与上下区的弱分层 |
| 分类筛选 | page_bg(米黄) |
与统计区连续 |
| 内容列表 | page_bg(米黄) |
主内容区 |
header_bg 和 page_bg 都是米黄色系的微差色——header_bg 略浅(#FBFAF4)、page_bg 略深(#F7F4EC)。这种"同色系微差"的分层比"强对比色分层"更柔和,符合食材保鲜的"温暖厨房"视觉调性。
Tab 栏底部的 1vp 边框(.borderWidth({ bottom: 1 }).borderColor($r('app.color.card_border')))是唯一的强分层线——Tab 栏是"导航区",下方是"内容区",这两个区有功能性差异,用边框线明确分隔。
二、双 Tab 栏的交互实现
2.1 Tab 栏的结构
Row() {
ForEach(this.tabs, (tab: string, index?: number) => {
Text(tab)
.fontSize(15)
.fontWeight(this.currentTab === index ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.currentTab === index ? $r('app.color.primary') : $r('app.color.text_hint'))
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.onClick(() => {
this.currentTab = index as number
})
if (index === 0) {
Text('·')
.fontColor($r('app.color.divider'))
.margin({ left: 6, right: 6 })
}
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
.borderWidth({ bottom: 1 })
.borderColor($r('app.color.card_border'))
.backgroundColor($r('app.color.header_bg'))
Tab 栏用 Row 容器 + ForEach 渲染两个 Tab 文本(“保鲜周期"和"常温保存”),中间用"·"分隔符。这是 ArkUI 实现 Tab 栏的轻量级方案——不用 Tabs 组件而用 Row + Text + onClick,理由是:
- 样式自由:
Text的字号、字重、字色可以精细控制,Tabs组件的样式定制较重 - 分隔符灵活:两个 Tab 中间加"·"分隔符,
Tabs组件不支持这种自定义分隔 - 交互简单:Tab 切换只是
this.currentTab = index,不需要Tabs的滑动切换等复杂行为
.justifyContent(FlexAlign.Center) 让 Tab 居中排列——两个 Tab 加分隔符的总宽度小于屏幕宽度,居中比左对齐更视觉平衡。
2.2 Tab 选中态的视觉反馈
Tab 选中态靠两个属性区分:
| 属性 | 选中态 | 未选中态 |
|---|---|---|
fontWeight |
Bold(粗) |
Normal(细) |
fontColor |
primary(绿) |
text_hint(灰) |
选中态用"粗 + 绿"双重强调,未选中用"细 + 灰"双重弱化。这种"字重 + 字色"的双重区分比单纯字色更易辨识——色弱用户即使字色辨识失败,字重差异仍能辨识。
字色用 $r('app.color.primary') 引用资源——primary 是色板的主色(暖绿 #4A8B3C),与未选中的 text_hint(灰 #9A9884)形成"鲜活 vs 沉默"的对比,符合"选中=活跃"的认知隐喻。
2.3 Tab 切换的响应式机制
.onClick(() => {
this.currentTab = index as number
})
Tab 的点击事件把 currentTab 设为对应索引。currentTab 是 @State 装饰的状态变量,值变化时鸿蒙的响应式系统会自动触发依赖该变量的 UI 重新渲染——具体到本应用,依赖 currentTab 的渲染点有:
- Tab 栏的选中态视觉反馈(
this.currentTab === index) - 顶部标题的项目计数 chip(
this.currentTab === 0 ? 'N项' : 'M项') - 概览统计栏的"不耐冷冻"统计(
if (this.currentTab === 0)分支) - 内容列表的 ForEach 数据集(
if (this.currentTab === 0) { 渲染 MAINTENANCE_DATA } else { 渲染 PARTS_DATA })
一处 currentTab 变化触发 4 处 UI 同步更新——这是 ArkUI 响应式系统的核心机制。开发者只需声明"UI 依赖什么状态",状态变化时系统自动算出"哪些 UI 需要更新",不需要手动调用 setText、setColor 等命令式更新。
2.4 ForEach 的 index 参数
ForEach(this.tabs, (tab: string, index?: number) => { ... })
ForEach 的第二个参数是数组索引,类型是 number | undefined(可选)。@State 的 currentTab: number = 0 是 number 类型,所以比较时要 index as number 强制断言——ArkTS 的类型系统对 undefined 比较严格,number === undefined 是类型不匹配。
if (index === 0) 的判断在第一个 Tab 后插入分隔符"·"——这是 ForEach 内的条件渲染,ArkUI 允许在 ForEach 的渲染回调里用 if 语句控制是否渲染某些节点。这个 if 在第一项后插入分隔符,第二项后不插,形成"A · B"的视觉分隔。
2.5 Tab 栏与 Tab 组件的取舍
鸿蒙 ArkUI 提供了官方 Tabs 组件:
Tabs() {
TabContent() { ... }.tabBar('保鲜周期')
TabContent() { ... }.tabBar('常温保存')
}
但本应用没用 Tabs 组件而手搓 Row + Text + onClick,取舍理由:
| 方案 | 优点 | 缺点 |
|---|---|---|
Tabs 组件 |
内置滑动切换、内置 ViewPager | 样式定制重、不支持中间分隔符 |
| 手搓 Row | 样式自由、分隔符灵活 | 无滑动切换、需手动写交互逻辑 |
本应用是"信息查询"工具,用户主动点 Tab 切换,不需要滑动切换的"被动浏览"交互。手搓方案的样式自由度(中间"·"分隔符、字重字色双重强调)比滑动切换更重要。
三、保鲜周期与常温保存的视图切换
3.1 内容列表的条件渲染
Scroll() {
Column({ space: 10 }) {
if (this.currentTab === 0) {
ForEach(this.getFilteredMaintenance(), (item: MaintenanceItem) => {
this.MaintenanceCard(item)
})
} else {
ForEach(this.getFilteredParts(), (item: PartItem) => {
this.PartCard(item)
})
}
// 底部图例
Row({ space: 16 }) { ... }
}
.padding({ left: 12, right: 12, top: 4, bottom: 4 })
}
.layoutWeight(1)
.scrollBar(BarState.Auto)
.backgroundColor($r('app.color.page_bg'))
内容列表用 if (this.currentTab === 0) 切换两个 ForEach——Tab 0 渲染保鲜周期卡片(MaintenanceCard),Tab 1 渲染常温保存卡片(PartCard)。这是 ArkUI 的条件渲染——if 语句控制是否渲染某个分支,if 的条件变化时整个分支重建或销毁。
条件渲染的特点是"完全销毁/重建"——切到 Tab 1 时,Tab 0 的所有卡片节点销毁(释放内存),Tab 1 的所有卡片节点新建。这种彻底的切换适合本应用——两个 Tab 的数据集和卡片结构都不同(保鲜周期有冷冻进度条、常温保存有干燥进度条),不存在"复用 DOM 节点"的场景。
对比 Tabs 组件的 TabContent 默认是"懒加载 + 缓存"——切走时 Tab 内容不销毁,切回时直接显示。本应用手搓方案用条件渲染是"销毁重建",性能上略低但内存更省。对于 20 条数据的轻量列表,销毁重建的开销可忽略。
3.2 底部图例的恒定渲染
// 底部图例
Row({ space: 16 }) {
this.LegendDot($r('app.color.primary'), '冷藏天数')
this.LegendDot($r('app.color.accent'), '冷冻天数')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 8, bottom: 24 })
底部图例在 if/else 之外,所以无论 Tab 0 还是 Tab 1 都渲染。但图例的文字依赖 Tab——Tab 0 是"冷藏天数/冷冻天数",Tab 1 应该是"常温天数/干燥天数"。这是本应用的一个设计取舍——图例文字硬编码为"冷藏/冷冻",在 Tab 1 显示时会误导。
更严谨的做法是图例文字也条件渲染:
if (this.currentTab === 0) {
this.LegendDot($r('app.color.primary'), '冷藏天数')
this.LegendDot($r('app.color.accent'), '冷冻天数')
} else {
this.LegendDot($r('app.color.primary'), '常温天数')
this.LegendDot($r('app.color.accent'), '干燥天数')
}
当前实现的取舍是接受"图例文字在 Tab 1 略有误导"的小瑕疵,换取代码简洁——少一个 if 分支。这是工程上"完美 vs 简洁"的权衡,本应用选简洁。
3.3 Column({ space: 10 }) 的间距语义
内容列表的 Column({ space: 10 }) ——space 参数是子节点之间的垂直间距,10vp 让每张卡片之间有 10vp 间隔。
space 比给每个卡片单独设 margin({ bottom: 10 }) 更好——space 是容器级的统一间距,加新卡片自动继承;单独设 margin 要每张卡片都写,且最后一张卡片的底部 margin 会造成"底部多余间距"。
space 的间距只作用于子节点之间,第一个子节点上方无间距、最后一个下方无间距。这种"内部间距"的语义让列表的 padding(外间距)和 space(内间距)分离——外层 Column 用 .padding({ top: 4, bottom: 4 }) 设上下外间距 4vp,内层 space 10vp 设卡片间内间距。
四、ForEach 列表渲染机制
4.1 ForEach 的数据驱动渲染
ForEach(this.getFilteredMaintenance(), (item: MaintenanceItem) => {
this.MaintenanceCard(item)
})
ForEach 的第一个参数是数据数组,第二个参数是渲染回调——回调对每个数组元素渲染一个 UI 节点。本应用的数据源是 getFilteredMaintenance() 的返回值,这个方法根据 activeCategory 过滤数据:
private getFilteredMaintenance(): MaintenanceItem[] {
if (this.activeCategory === '全部') {
return MAINTENANCE_DATA
}
return MAINTENANCE_DATA.filter((item: MaintenanceItem) => item.category === this.activeCategory)
}
activeCategory 是 @State,变化时 getFilteredMaintenance() 的返回值变化,ForEach 检测到数据源变化触发重新渲染——这就是"分类筛选触发列表更新"的响应式链条。
ForEach 的内部机制是"差量渲染"——新数据源与旧数据源对比,只更新差异部分。本应用里分类筛选时,"全部"切换到"冷藏"会移除非冷藏的卡片、保留冷藏的卡片,而不是销毁全部重建。这种差量渲染对性能友好——20 条切换到 5 条,只销毁 15 条而不重建 5 条。
4.2 @Builder 的渲染复用
@Builder
MaintenanceCard(item: MaintenanceItem) {
Column() {
// ... 40 行的卡片布局
}
.width('100%')
.padding(12)
.backgroundColor($r('app.color.card_bg'))
.borderRadius(12)
.borderWidth(1)
.borderColor($r('app.color.card_border'))
}
@Builder 是 ArkUI 的 UI 复用单元——定义一个渲染函数,参数传入数据,调用时渲染对应 UI。ForEach 内调用 this.MaintenanceCard(item) 把每个数据项渲染成卡片。
@Builder 相对独立 build() 方法的优势是:
- 复用:一个 Builder 可在多处调用,本应用 MaintenanceCard 在 ForEach 里被调用 20 次
- 参数化:Builder 接收参数,不同参数渲染不同 UI——每张卡片的数据不同
- 可读性:build() 方法不会因为列表渲染膨胀,列表渲染逻辑封装在 Builder 里
@Builder 的参数是强类型——item: MaintenanceItem 要求调用时传入 MaintenanceItem 实例,类型不匹配编译失败。这是 ArkTS 类型系统对 Builder 的保护。
4.3 @Builder vs Component 的取舍
ArkUI 还有 @Component 装饰器——把 UI 封装成独立组件。@Builder vs @Component 的取舍:
| 方案 | 优点 | 缺点 |
|---|---|---|
@Builder |
调用简单、共享 @State、轻量 | 不能独立复用、无独立生命周期 |
@Component |
独立组件、可独立 @State、可独立 export | 调用要 import、状态隔离要 @Prop/@Link |
本应用用 @Builder 而非 @Component,理由是:
- 卡片不需要独立状态:卡片的渲染完全由传入的
item数据决定,无独立交互状态 - 共享 @State:Builder 内访问
this.activeCategory、this.currentTab等 @State 直接通过,不需要 @Prop/@Link 传参 - 轻量:单页面应用的卡片封装成 Builder 足够,独立 Component 是过度工程
@Component 适合"独立可复用、有独立状态"的组件——如自定义弹窗、独立播放器等。本应用的卡片是"渲染数据"的轻量单元,Builder 更合适。
五、Scroll 容器的滚动策略
5.1 内容列表的纵向滚动
Scroll() {
Column({ space: 10 }) { ... }
}
.layoutWeight(1)
.scrollBar(BarState.Auto)
.backgroundColor($r('app.color.page_bg'))
内容列表用 Scroll 容器包裹 Column,让超出屏幕高度的内容可纵向滚动。.layoutWeight(1) 让 Scroll 占据剩余高度(顶部区块外的全屏),.scrollBar(BarState.Auto) 让滚动条自动显示——滚动时出现、停止时消失。
Scroll 的滚动方向默认是纵向(Y 轴),与本应用的列表布局匹配。如果列表是横向的(如卡片一行排开),要设 .scrollable(ScrollDirection.Horizontal) 改方向。
BarState 有三个值:
| 值 | 行为 | 本应用使用场景 |
|---|---|---|
Auto |
滚动时显示、停止时隐藏 | 内容列表(长列表,需要滚动提示) |
Off |
永不显示 | 分类筛选(短距离横滚,不需要提示) |
On |
永远显示 | 本应用未用 |
内容列表用 Auto——20 条卡片滚动 5-6 屏,滚动条让用户感知"当前位置和总长度"。分类筛选用 Off——5 个 chip 横滚距离很短,滚动条反而干扰视觉。
5.2 分类筛选的横向滚动
Scroll() {
Row({ space: 8 }) {
ForEach(this.categories, (cat: string) { ... })
}
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.backgroundColor($r('app.color.page_bg'))
分类筛选用 Scroll + Row 实现横向滚动——5 个 chip 一字排开,超出屏幕宽度时横滚。.scrollable(ScrollDirection.Horizontal) 设滚动方向为横向。
横滚的触发条件是 chip 总宽度 > 屏幕宽度。5 个 chip(“全部/冷藏/冷冻/常温/蔬果”)每个约 60-80vp(含 padding),总宽 300-400vp。在 360vp 窄屏上可能触发横滚(特别是右侧 padding 后),在 720vp 宽屏上一字排开有余裕不滚动。
scrollBar(BarState.Off) 隐藏滚动条——5 个 chip 的横滚距离很短,滚动条反而干扰视觉。这是"短距离滚动隐藏滚动条"的惯用法。
5.3 Scroll vs List 的取舍
鸿蒙 ArkUI 还有 List 组件专门用于长列表:
List() {
ForEach(...) { ListItem() { ... } }
}
List vs Scroll + ForEach 的取舍:
| 方案 | 优点 | 缺点 |
|---|---|---|
List |
内置性能优化(节流渲染、复用 ListItem) | 样式定制重、不支持非 ListItem 子节点 |
Scroll + ForEach |
灵活、任意子节点 | 长列表性能略低(无复用) |
本应用用 Scroll + ForEach,理由是:
- 列表不长:20 条卡片不是"长列表",Scroll 全渲染无性能压力
- 底部图例:列表末尾有图例 Row,List 只能放 ListItem,图例不是 ListItem 形态
- 样式自由:卡片的间距、padding、圆角等样式靠 Column 的 space 和卡片自身的样式控制,List 的 ListItem 样式约束较多
List 适合"50+ 条同构列表 + 性能敏感"的场景——如通讯录、消息列表。本应用是"20 条异构列表(卡片 + 图例)",Scroll 更合适。
5.4 Scroll 的嵌套约束
本应用有两个 Scroll——分类筛选(横滚)和内容列表(纵滚)。它们是平级关系(都在顶层 Column 内),不是嵌套。鸿蒙 ArkUI 对 Scroll 嵌套有约束——外层纵滚内层横滚是允许的,但外层纵滚内层也纵滚会被外层吃掉滚动事件,内层不滚。
本应用两个 Scroll 方向不同(一横一纵),即使嵌套也安全。但当前设计是平级,更简单——分类筛选和内容列表各自管自己的滚动,互不干扰。
六、Blank 弹性占位与 layoutWeight 权重布局
6.1 顶部标题栏的 Blank 弹性
Row() {
Column() {
Text('食材保鲜手册')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text('保鲜周期 · 常温保存时长')
.fontSize(12)
.fontColor($r('app.color.text_hint'))
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Blank()
Text(this.currentTab === 0 ?
`${this.getFilteredMaintenance().length}项` :
`${this.getFilteredParts().length}项`)
.fontSize(12)
.fontColor($r('app.color.primary'))
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor($r('app.color.tag_normal_bg'))
.borderRadius(10)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 10 })
.backgroundColor($r('app.color.header_bg'))
顶部标题栏用 Row 内 Blank() 把左右两段推开——左段是应用名 + 副标题,右段是项目计数 chip,中间 Blank() 吃掉剩余宽度。
Blank() 是 ArkUI 的弹性空白节点——在 Row 里吃掉剩余宽度,让左右两端对齐。这是 Flexbox 经典的"两端对齐 + 中间弹性"模式。
Blank() 的行为:
- 在 Row 里:吃掉剩余宽度,左右推开
- 在 Column 里:吃掉剩余高度,上下推开
如果不用 Blank(),左段和右段会紧挨——应用名和计数 chip 之间没有间隔,视觉拥挤。Blank() 自动填充让两端对齐。
6.2 layoutWeight 的权重布局
// 进度条行
Row({ space: 6 }) {
Column() {
Text('冷藏') ...
Stack() { ... }
}
.layoutWeight(1)
Column() {
Text('冷冻') ...
Stack() { ... }
}
.layoutWeight(1)
}
layoutWeight(n) 是 ArkUI 的弹性布局权重——子节点按权重分配剩余空间。本应用进度条行的两个 Column 各 layoutWeight(1),权重相同,等分剩余宽度——每个进度条占 Row 的 50%。
layoutWeight vs width('50%') 的区别:
| 方案 | 行为 |
|---|---|
layoutWeight(1) 各 Column |
按权重分配剩余宽度(减去已占宽度后均分) |
width('50%') 各 Column |
各占 Row 的 50% |
本应用进度条行用 layoutWeight(1)——Row({ space: 6 }) 的 6vp 间距会先被扣除,剩余宽度再均分给两个 Column。如果用 width('50%'),两个 50% + 间距 6vp 会超过 100%,布局错位。
layoutWeight 的优势是"自动适应间距和固定宽度子节点"——剩余空间 = 总宽 - 固定宽度 - 间距,按权重分给弹性子节点。这是 Flexbox 的核心算法,ArkUI 完整支持。
6.3 卡片内的弹性组合
每张卡片内部的布局是"Blank + layoutWeight"的组合:
- 顶部 Row:左标题组(自适应)+ Blank(弹性)+ 右数值组(自适应),两端对齐
- 进度条 Row:左进度条 Column(layoutWeight 1)+ 右进度条 Column(layoutWeight 1),等分
- 底部备注 Row:左标签(自适应)+ 右文字(layoutWeight 1),文字占满剩余
这种"自适应 + 弹性"的组合让卡片在不同宽度下都保持合理比例——窄屏上左段右段挤紧、弹性段窄;宽屏上左段右段拉散、弹性段宽。这是响应式布局的核心——不用断点,靠弹性权重自然适应。
七、响应式取舍:从 360vp 到 720vp
7.1 无断点的响应式
本应用没有显式的响应式断点——没有 if (screenWidth > 600) { ... } else { ... } 的条件渲染。所有布局都用百分比和 layoutWeight,自然适应不同屏幕宽度。
这适合工具型应用——内容是固定卡片列表,不需要"窄屏一列宽屏两列"的响应式重构。但仍有几个点值得分析:
7.2 卡片宽度的响应性
每张卡片 .width('100%') 占满列表容器全宽。列表容器是 Scroll 内的 Column,宽度受 Scroll 约束,Scroll 又受外层 Column 约束到屏幕宽度。
因此卡片宽度 = 屏幕宽度 - 列表 padding(左右各 12vp)= 屏幕宽度 - 24vp。在 360vp 窄屏上是 336vp,在 720vp 宽屏上是 696vp。
卡片内部布局靠 Blank 和 layoutWeight 自适应:
- 顶部 Row:左标题组 + Blank + 右数值组,两端对齐
- 进度条 Row:两条 Column 各 layoutWeight(1),等分
- 底部备注 Row:标签 + layoutWeight(1) 的文字
这种"百分比 + layoutWeight + Blank"的三件套让卡片在任何宽度下都保持合理内部比例,不会"窄屏挤、宽屏散"。
7.3 进度条长度的响应性
进度条长度 = 卡片宽度 × 进度条 Column 宽度比例 × 百分比。
在 360vp 窄屏:
- 卡片宽 336vp
- 进度条 Column 宽 = (336 - 24 padding - 6 space) / 2 = 153vp
- 100% 进度条 = 153vp,50% = 76.5vp
在 720vp 宽屏:
- 卡片宽 696vp
- 进度条 Column 宽 = (696 - 24 - 6) / 2 = 333vp
- 100% 进度条 = 333vp,50% = 166.5vp
宽屏进度条更长,视觉对比更明显;窄屏进度条较短但仍可辨识。这是"用百分比而非固定 vp"的响应式红利——进度条长度自然跟随屏幕宽度。
7.4 横滚 chip 列的响应性
5 个 chip 的总宽度约 300-400vp,在 360vp 窄屏上可能超出屏幕宽度触发横滚;在 720vp 宽屏上一字排开有余裕不滚动。
scrollBar(BarState.Off) 在两种屏幕都隐藏滚动条——窄屏滚动时无滚动条提示(chip 列很短,用户左右滑能感知),宽屏不滚动时无残留滚动条。
7.5 顶部标题栏的响应性
顶部标题栏用 Row 内 Blank() 把左右两段推开——左段和右段宽度由内容决定,Blank() 自动调整中段宽度让两端对齐。在 360vp 窄屏和 720vp 宽屏上,左段和右段宽度都由内容决定,Blank() 自动吃掉中段剩余,两端对齐效果一致。
这是 Flexbox 经典的"两端对齐 + 中间弹性"模式,在 ArkUI 里用 Blank() 实现。这种布局对屏幕宽度天然免疫——任何宽度下都两端对齐。
八、ArkUI 声明式布局的核心抽象
8.1 三轴容器
ArkUI 的核心布局容器对应三个轴:
| 容器 | 轴 | 子节点排列 | 本应用使用 |
|---|---|---|---|
Column |
Y 轴 | 垂直堆叠 | 顶层骨架、卡片内分组 |
Row |
X 轴 | 横向排列 | 标题栏、Tab 栏、进度条行 |
Stack |
Z 轴 | 重叠叠层 | 进度条(背景 + 前景) |
选错轴会导致布局完全错位——进度条用 Stack 才能叠背景前景,如果用 Column 会变成两条独立条而不是一条进度条。
8.2 弹性机制
ArkUI 的弹性布局有两个机制:
| 机制 | 用途 | 本应用使用 |
|---|---|---|
Blank() |
吃掉剩余空间 | 标题栏两端对齐 |
layoutWeight(n) |
按权重分配剩余 | 进度条等分、卡片文字占满 |
两者都是"剩余空间分配"——Blank() 是"全吃",layoutWeight 是"按权重分吃"。本应用标题栏用 Blank(一段吃掉中段),进度条行用 layoutWeight(两段按 1:1 分)。
8.3 滚动容器
ArkUI 的滚动容器有 Scroll 和 List,本应用都用 Scroll:
| 场景 | 方向 | 滚动条 | 内容形态 |
|---|---|---|---|
| 内容列表 | 纵向 | Auto | 20 条卡片 + 图例 |
| 分类筛选 | 横向 | Off | 5 个 chip |
Scroll 的灵活性是任意子节点(卡片、图例、chip),List 的优势是长列表性能。本应用列表不长且需要混合内容,Scroll 更合适。
九、本篇小结
本章从 build() 顶层骨架讲到响应式取舍,完整覆盖了「食材保鲜手册」ArkUI 声明式布局的工程实现。关键点归纳:
- 单页面顶层骨架:Column 垂直堆叠五区块,固定头 + 弹性身,layoutWeight(1) 让列表吃剩余高度
- 双 Tab 手搓实现:Row + ForEach + onClick 而非 Tabs 组件,样式自由 + 分隔符灵活
- 条件渲染视图切换:if (currentTab === 0) 切两个 ForEach,彻底销毁重建适合异构数据
- ForEach 数据驱动:@State activeCategory 变化 → getFiltered 变化 → ForEach 差量渲染
- @Builder 渲染复用:卡片封装成 Builder,ForEach 内调用 20 次,参数化 + 共享 @State
- Scroll 双向滚动:内容列表纵滚 Auto、分类筛选横滚 Off,短距离滚动隐藏滚动条
- Blank + layoutWeight 弹性:标题栏两端对齐、进度条等分、卡片文字占满,无断点响应式
十、附录:本篇涉及的 ArkUI 布局 API
| API | 用途 | 本篇体现 |
|---|---|---|
Column({ space }) |
Y 轴堆叠容器 | 顶层骨架、卡片内分组 |
Row({ space }) |
X 轴排列容器 | 标题栏、Tab 栏、进度条行 |
Stack({ alignContent }) |
Z 轴叠层容器 | 进度条背景与前景叠 |
Blank() |
弹性空白 | 标题栏两端对齐 |
.layoutWeight(n) |
弹性布局权重 | 进度条等分、列表吃剩余 |
.width('100%') |
百分比宽度 | 卡片占满列表 |
.height('100%') |
百分比高度 | 顶层 Column 占满窗口 |
.justifyContent(FlexAlign.Center) |
主轴对齐 | Tab 栏居中、图例居中 |
.alignItems(HorizontalAlign.Start) |
交叉轴对齐 | 标题左对齐、数值右对齐 |
Scroll() |
滚动容器 | 内容列表、分类筛选 |
.scrollable(dir) |
滚动方向 | 分类筛选横滚 |
.scrollBar(state) |
滚动条显隐 | 内容 Auto、筛选 Off |
ForEach(arr, cb) |
数据驱动渲染 | Tab、分类筛选、卡片列表 |
if (cond) |
条件渲染 | Tab 视图切换、不耐冷冻警示 |
@Builder |
UI 复用单元 | MaintenanceCard、PartCard、LegendDot |
至此完整覆盖了「食材保鲜手册」ArkUI 声明式布局的工程实现。下篇将从保鲜周期与常温保存的双数据集驱动渲染继续展开。
更多推荐




所有评论(0)