本文基于 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 占满整个窗口——鸿蒙应用的窗口由 EntryAbilitywindowStage 创建,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_bgpage_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,理由是:

  1. 样式自由Text 的字号、字重、字色可以精细控制,Tabs 组件的样式定制较重
  2. 分隔符灵活:两个 Tab 中间加"·"分隔符,Tabs 组件不支持这种自定义分隔
  3. 交互简单: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 的渲染点有:

  1. Tab 栏的选中态视觉反馈(this.currentTab === index
  2. 顶部标题的项目计数 chip(this.currentTab === 0 ? 'N项' : 'M项'
  3. 概览统计栏的"不耐冷冻"统计(if (this.currentTab === 0) 分支)
  4. 内容列表的 ForEach 数据集(if (this.currentTab === 0) { 渲染 MAINTENANCE_DATA } else { 渲染 PARTS_DATA }

一处 currentTab 变化触发 4 处 UI 同步更新——这是 ArkUI 响应式系统的核心机制。开发者只需声明"UI 依赖什么状态",状态变化时系统自动算出"哪些 UI 需要更新",不需要手动调用 setTextsetColor 等命令式更新。

2.4 ForEach 的 index 参数

ForEach(this.tabs, (tab: string, index?: number) => { ... })

ForEach 的第二个参数是数组索引,类型是 number | undefined(可选)。@StatecurrentTab: number = 0number 类型,所以比较时要 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() 方法的优势是:

  1. 复用:一个 Builder 可在多处调用,本应用 MaintenanceCard 在 ForEach 里被调用 20 次
  2. 参数化:Builder 接收参数,不同参数渲染不同 UI——每张卡片的数据不同
  3. 可读性: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,理由是:

  1. 卡片不需要独立状态:卡片的渲染完全由传入的 item 数据决定,无独立交互状态
  2. 共享 @State:Builder 内访问 this.activeCategorythis.currentTab 等 @State 直接通过,不需要 @Prop/@Link 传参
  3. 轻量:单页面应用的卡片封装成 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,理由是:

  1. 列表不长:20 条卡片不是"长列表",Scroll 全渲染无性能压力
  2. 底部图例:列表末尾有图例 Row,List 只能放 ListItem,图例不是 ListItem 形态
  3. 样式自由:卡片的间距、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'))

顶部标题栏用 RowBlank() 把左右两段推开——左段是应用名 + 副标题,右段是项目计数 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 顶部标题栏的响应性

顶部标题栏用 RowBlank() 把左右两段推开——左段和右段宽度由内容决定,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 的滚动容器有 ScrollList,本应用都用 Scroll

场景 方向 滚动条 内容形态
内容列表 纵向 Auto 20 条卡片 + 图例
分类筛选 横向 Off 5 个 chip

Scroll 的灵活性是任意子节点(卡片、图例、chip),List 的优势是长列表性能。本应用列表不长且需要混合内容,Scroll 更合适。

九、本篇小结

本章从 build() 顶层骨架讲到响应式取舍,完整覆盖了「食材保鲜手册」ArkUI 声明式布局的工程实现。关键点归纳:

  1. 单页面顶层骨架:Column 垂直堆叠五区块,固定头 + 弹性身,layoutWeight(1) 让列表吃剩余高度
  2. 双 Tab 手搓实现:Row + ForEach + onClick 而非 Tabs 组件,样式自由 + 分隔符灵活
  3. 条件渲染视图切换:if (currentTab === 0) 切两个 ForEach,彻底销毁重建适合异构数据
  4. ForEach 数据驱动:@State activeCategory 变化 → getFiltered 变化 → ForEach 差量渲染
  5. @Builder 渲染复用:卡片封装成 Builder,ForEach 内调用 20 次,参数化 + 共享 @State
  6. Scroll 双向滚动:内容列表纵滚 Auto、分类筛选横滚 Off,短距离滚动隐藏滚动条
  7. 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 声明式布局的工程实现。下篇将从保鲜周期与常温保存的双数据集驱动渲染继续展开。

Logo

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

更多推荐