在这里插入图片描述

在这里插入图片描述

一、引言:为什么弹性布局是鸿蒙 UI 开发的基石

在移动端与多设备协同的时代,屏幕尺寸的碎片化已成为前端与原生开发必须面对的核心挑战。从 320px 宽的智能手表到 1440px 宽的平板,再到折叠屏展开前后的尺寸变化,应用界面必须具备「弹性」——即能够自动适应容器尺寸变化的能力,而不是为每一种屏幕尺寸硬编码一套布局规则。

HarmonyOS NEXT(API 24)在 ArkUI 框架中提供了一套完整的弹性布局体系,其核心容器 Flex 借鉴并超越了 CSS Flexbox 的设计思想,同时深度结合了 ArkTS 的声明式语法与响应式状态管理。与传统的 Row / Column 线性布局相比,Flex 容器提供了更精细的弹性控制:

  • flexGrow:控制子组件在主轴方向上的「放大」比例
  • flexShrink:控制子组件在主轴方向上的「缩小」比例
  • flexBasis:控制子组件在弹性分配前的「初始尺寸」
  • flexWrap:控制子组件能否换行(流式布局的关键)
  • layoutWeight:按权重比例分配容器的全部空间

本文将以一个完整的鸿蒙应用项目为依托,从基础的 Column 主轴对齐开始,逐步深入到 Flex 的四种方向模式(Row / RowReverse / Column / ColumnReverse),再到换行响应式布局和 flexGrow 比例增长,最后通过一个交互式演示组件收尾,构建一条完整的学习路径。


二、应用架构与页面导航

2.1 项目入口(EntryAbility)

鸿蒙应用的入口是 UIAbility 的子类。在 onWindowStageCreate 生命周期中,通过 windowStage.loadContent() 加载首页面:

// entry/src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onCreate');
  }

  onDestroy(): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onWindowStageCreate');
    windowStage.loadContent('pages/FlexGrowDemo', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'EnglishApp', 'Failed to load content: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'EnglishApp', 'Succeeded in loading content.');
    });
  }

  onWindowStageDestroy(): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onForeground');
  }

  onBackground(): void {
    hilog.info(DOMAIN, 'EnglishApp', 'Ability onBackground');
  }
}

要点说明

  • loadContent 的参数是一个页面路径,'pages/FlexGrowDemo' 对应 entry/src/main/ets/pages/FlexGrowDemo.ets 中用 @Entry 装饰的结构体。
  • 页面路径不需要文件扩展名 .ets,框架会自动解析。
  • hilog 是鸿蒙的日志工具,属于 @kit.PerformanceAnalysisKit 套件。

2.2 页面结构通用模板

每个演示页面都遵循相同的骨架结构:

├── 最外层 Column(充满全屏)
│   ├── 顶部标题区(固定,蓝底白字)
│   └── Scroll 滚动区域
│       └── 内层 Column
│           ├── 演示区块 1(白色圆角卡片)
│           ├── 演示区块 2
│           ├── ...
│           └── 技术说明面板

这种结构确保页面在长内容时能够上下滚动,而标题始终固定在顶部,不会随内容滚动。


三、Column + justifyContent:主轴垂直对齐布局

3.1 核心概念

Column 是 ArkUI 中最基础的垂直布局容器,它的主轴(Main Axis)方向是「从上到下」。与之配套的 justifyContent 属性控制子组件在主轴方向上的排列方式,常见的对齐值包括:

枚举值 效果 类比 CSS
FlexAlign.Start 顶部起始排列 flex-start
FlexAlign.Center 垂直居中 center
FlexAlign.End 底部对齐 flex-end
FlexAlign.SpaceBetween 两端对齐,子项间等距 space-between
FlexAlign.SpaceAround 每个子项左右间距相等 space-around
FlexAlign.SpaceEvenly 子项间及两端间距全相等 space-evenly

3.2 完整代码实现

// Index.ets — Column + justifyContent(FlexAlign.Start) 演示
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'ColumnStartDemo';

interface InfoItem {
  title: string;
  desc: string;
}

@Component
struct InfoCard {
  private item: InfoItem = { title: '', desc: '' };
  private index: number = 0;

  build() {
    Column() {
      Text(this.item.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1a1a2e')
        .lineHeight(22)

      Text(this.item.desc)
        .fontSize(13)
        .fontColor('#666666')
        .lineHeight(20)
        .margin({ top: 6 })
    }
    .alignItems(HorizontalAlign.Start)
    .width('100%')
    .padding(14)
    .backgroundColor('#f8f9fc')
    .borderRadius(10)
    .shadow({ radius: 4, color: '#20000000', offsetX: 0, offsetY: 2 })
    .margin({ bottom: 10 })
  }
}

@Entry
@Component
struct ColumnStartPage {
  private readonly infoList: InfoItem[] = [
    { title: '📌 系统通知', desc: '您的鸿蒙应用已通过安全检测,点击查看详情。' },
    { title: '📊 数据报告', desc: '本周活跃用户较上周增长 12%,持续优化中。' },
    { title: '⚙️ 版本更新', desc: 'v3.2.0 发布:新增弹性布局组件示例。' },
    { title: '🎯 优化建议', desc: '检测到 3 处可优化项,建议在闲时处理。' },
  ];

  build() {
    Column() {
      // 标题区
      Column() {
        Text('📐 Column + justifyContent(Start)')
          .fontSize(20).fontWeight(FontWeight.Bold)
          .fontColor('#ffffff').lineHeight(28)
        Text('主轴(垂直)顶部起始 · 子组件从容顶依次排列')
          .fontSize(12).fontColor('#cce0ff').margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start)
      .width('100%')
      .padding({ top: 20, bottom: 16, left: 20, right: 20 })
      .backgroundColor('#2d5f8a')

      // 核心演示区
      Column() {
        // ... 内容卡片列表 ...
      }
      .alignItems(HorizontalAlign.Start)      // 交叉轴左对齐
      .justifyContent(FlexAlign.Start)        // ★ 主轴顶部起始 ★
      .width('100%')
      .height(0)
      .layoutWeight(1)
      .padding(16)
      .backgroundColor('#ffffff')
      .borderRadius(12)
      .margin({ left: 12, right: 12, top: 10, bottom: 12 })
      .shadow({ radius: 6, color: '#1a000000', offsetX: 0, offsetY: 2 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#eef2f7')
  }
}

3.3 关键理解

  1. 主轴方向:Column 的主轴是垂直方向,justifyContent 控制垂直对齐。
  2. 交叉轴方向:Column 的交叉轴是水平方向,alignItems 控制水平对齐(HorizontalAlign.Start / Center / End)。
  3. layoutWeight(1):让 Column 占满父容器的剩余高度,确保 justifyContent 的效果可见——如果 Column 高度等于内容高度,那么 Start、Center、End 是看不出区别的。
  4. 阴影的性能shadow 属性在 API 24 中已经过硬件加速优化,但过多卡片使用阴影仍会影响列表滚动性能,建议对长列表使用 shadow 而非 elevation

3.4 应用场景

  • 信息流列表(通知、动态、消息)
  • 设置页的垂直分组
  • 表单页面的标签 + 输入框组合
  • 纵向导航菜单

四、Flex 弹性容器与四种 flexDirection 方向

Flex 是比 Column / Row 更底层、更灵活的弹性容器。ColumnRow 本质上是 Flex 的特殊封装——Column 等效于 flexDirection(FlexDirection.Column)Flex 容器,Row 等效于 flexDirection(FlexDirection.Row)Flex 容器。

Flex 还额外支持 RowReverseColumnReverse 两种反向模式,这是 RowColumn 容器无法直接做到的特性。

4.1 四种方向对比

flexDirection 值 排列方向 代码首项位置 典型场景
FlexDirection.Row 从左→右 最左侧 水平导航栏
FlexDirection.RowReverse 从右→左 最右侧 RTL语言界面(阿拉伯语)
FlexDirection.Column 从上→下 最顶部 表单 / 纵向列表
FlexDirection.ColumnReverse 从下→上 最底部 聊天消息列表

4.2 Row 与 RowReverse 对比实现

以下是展示两种模式差异的核心代码片段:

// ═══════ Row:从左到右 ═══════
Flex() {
  FlexItem({ label: 'A', color: '#5B9BD5' })
  FlexItem({ label: 'B', color: '#70AD47' })
  FlexItem({ label: 'C', color: '#FFC000' })
}
.flexDirection(FlexDirection.Row)        // ← 正常方向
.alignItems(ItemAlign.Center)
.width('100%').height(56)

// ═══════ RowReverse:从右到左 ═══════
Flex() {
  FlexItem({ label: 'A(先写)', color: '#5B9BD5' })
  FlexItem({ label: 'B(中间)', color: '#70AD47' })
  FlexItem({ label: 'C(后写)', color: '#FFC000' })
}
.flexDirection(FlexDirection.RowReverse) // ← 反向
.alignItems(ItemAlign.Center)
.width('100%').height(56)

输出结果

  • Row:[A] [B] [C](从左到右)
  • RowReverse:[C] [B] [A](从右到左,代码靠前的 A 显示在最右侧)

4.3 ColumnReverse 的核心应用场景:聊天列表

ColumnReverse 最典型的应用场景是聊天消息列表:最新消息显示在底部,历史消息在上方。这与用户使用聊天应用的习惯完全一致——进入聊天界面时视线自然落在最新的消息上。

Flex() {
  // 最早的消息在代码顶部,显示在列表顶部
  ChatBubble({ msg: '早上好!今天有什么安排?', isSelf: false })
  // 对方的回复
  ChatBubble({ msg: '下午三点有个团队会议。', isSelf: true })
  // 确认消息
  ChatBubble({ msg: '收到,资料我提前准备好。', isSelf: false })
  // ★ 最新消息在代码最后,显示在列表最底部 ★
  ChatBubble({ msg: '好的,大会议室见!', isSelf: true })
}
.flexDirection(FlexDirection.ColumnReverse)  // ← ★ 核心 ★
.alignItems(ItemAlign.Stretch)
.width('100%').height(300)

关键理解:在 ColumnReverse 中,justifyContent(FlexAlign.Start) 等效于「底部对齐」,FlexAlign.End 等效于「顶部对齐」。这是因为 Start 始终指向主轴的起始位置,而 ColumnReverse 的起始位置在底部。

4.4 容易混淆的细节

  • RowReverse 只反转排列顺序,不反转每个组件内部的内容。组件的文字方向、图片朝向等不受影响。
  • alignItems 不受反向影响。无论 Row 还是 RowReverse,alignItems 始终控制交叉轴(垂直方向)的对齐方式。
  • flexGrow/flexShrink 在反向模式下仍作用于主轴方向。RowReverse 下 flexGrow 仍控制宽度,ColumnReverse 下仍控制高度。

五、Flex + Wrap:响应式卡片流布局

5.1 从「线性排列」到「流式排列」

当子组件数量多到一行放不下时,普通 Flex(不换行)会尝试压缩所有子组件以适应容器宽度。但在很多场景下,我们希望子组件在空间不足时自动「换行」到下一行,形成流式布局——这就是 flexWrap(FlexWrap.Wrap) 的作用。

5.2 商品卡片流完整实现

// 商品数据接口
interface ProductItem {
  id: number;
  name: string;
  price: number;
  sold: number;
  tag: string;
  color: string;
  emoji: string;
}

// 预设商品数据
const PRODUCTS: ProductItem[] = [
  { id: 1,  name: '无线蓝牙耳机',  price: 299,  sold: 18324,
    tag: '数码', color: '#5B9BD5', emoji: '🎧' },
  { id: 2,  name: '智能手表 Pro',  price: 1299, sold: 6592,
    tag: '穿戴', color: '#70AD47', emoji: '⌚' },
  { id: 3,  name: '便携充电宝',    price: 99,   sold: 47531,
    tag: '配件', color: '#FFC000', emoji: '🔋' },
  { id: 4,  name: '降噪头戴耳机',  price: 899,  sold: 2187,
    tag: '数码', color: '#ED7D31', emoji: '🎵' },
  { id: 5,  name: '氮化镓充电器',  price: 149,  sold: 32098,
    tag: '配件', color: '#FF6B6B', emoji: '⚡' },
  { id: 6,  name: '运动手环 8',    price: 199,  sold: 28314,
    tag: '穿戴', color: '#9B59B6', emoji: '💪' },
  { id: 7,  name: '无线鼠标',      price: 79,   sold: 51203,
    tag: '外设', color: '#1ABC9C', emoji: '🖱️' },
  { id: 8,  name: '机械键盘 K3',   price: 449,  sold: 8956,
    tag: '外设', color: '#E91E63', emoji: '⌨️' },
  { id: 9,  name: 'USB-C 扩展坞',  price: 259,  sold: 11782,
    tag: '配件', color: '#2C3E50', emoji: '🔌' },
  { id: 10, name: '平板支架',      price: 69,   sold: 38920,
    tag: '配件', color: '#16A085', emoji: '📱' },
  { id: 11, name: '蓝牙音箱 Mini', price: 169,  sold: 22345,
    tag: '数码', color: '#F39C12', emoji: '🔊' },
  { id: 12, name: '笔记本内胆包',  price: 89,   sold: 15678,
    tag: '包袋', color: '#8E44AD', emoji: '💼' },
];

商品卡片组件,使用 layoutWeight 控制在行内的宽度比例:

@Component
struct ProductCard {
  private product: ProductItem = PRODUCTS[0];
  @Link cardSizeMode: number;   // 0=小(3列) / 1=中(2列) / 2=大(1列)

  build() {
    Column() {
      // 卡片顶部:图标 + 标签
      Row() {
        Text(this.product.emoji).fontSize(this.cardSizeMode === 2 ? 36 : 26)
        Text(this.product.tag)
          .fontSize(11).fontColor('#ffffff')
          .backgroundColor(this.product.color)
          .padding({ left: 8, right: 8, top: 2, bottom: 2 })
          .borderRadius(10).margin({ left: 6 })
      }
      .width('100%').justifyContent(FlexAlign.SpaceBetween)

      // 商品名称
      Text(this.product.name)
        .fontSize(this.cardSizeMode === 2 ? 18 : 14)
        .fontWeight(FontWeight.Bold).fontColor('#1a1a2e')
        .margin({ top: 8 })
        .width('100%')

      // 价格和销量
      Row() {
        Text(`¥${this.product.price}`)
          .fontSize(this.cardSizeMode === 2 ? 22 : 18)
          .fontWeight(FontWeight.Bold).fontColor('#e74c3c')
        Text(`已售 ${this.product.sold}`)
          .fontSize(11).fontColor('#999').margin({ left: 8 })
      }
      .width('100%').margin({ top: 6 })

      // 大尺寸模式下展开更多信息
      if (this.cardSizeMode === 2) {
        Button('加入购物车')
          .width('100%').height(36)
          .backgroundColor(this.product.color)
          .fontColor('#ffffff').fontSize(14)
          .borderRadius(8).margin({ top: 10 })
      }
    }
    .padding(this.cardSizeMode === 2 ? 16 : 12)
    .backgroundColor('#ffffff')
    .borderRadius(14)
    .shadow({ radius: 6, color: '#15000000', offsetX: 0, offsetY: 3 })
    .layoutWeight(1)               // ★ 同一行中所有 layoutWeight(1) 等分宽度 ★
    .margin(6)
  }
}

核心响应式容器:

Flex() {
  ForEach(PRODUCTS, (product: ProductItem) => {
    ProductCard({
      product: product,
      cardSizeMode: this.cardSizeMode   // @State 驱动
    })
  })
}
.flexDirection(FlexDirection.Row)      // 主轴水平
.flexWrap(FlexWrap.Wrap)               // ★ 允许换行(核心)★
.justifyContent(FlexAlign.Start)       // 行内左对齐
.alignItems(ItemAlign.Start)           // 交叉轴顶部对齐
.width('100%')

5.3 layoutWeight 的工作原理

layoutWeightflexGrow 的关键区别在于:

属性 分配对象 计算公式
flexGrow 剩余空间(容器宽 − 所有子项内容宽) 内容宽 + 剩余空间 × (growN / Σgrow)
layoutWeight 容器全部空间 容器宽 × (weightN / Σweight)

在换行布局中,layoutWeight 更常用,因为每行都会独立计算权重分配——这意味着每行的总宽度都会被均分到行内的子项上,不会受到其他行子项「内容宽度」不同的干扰。

5.4 密度切换的响应式设计

通过 @State cardSizeMode 驱动三种卡片密度:

@State cardSizeMode: number = 0;   // 0=小(3列) / 1=中(2列) / 2=大(1列)

// 三个按钮切换密度
Row() {
  ForEach([0, 1, 2], (mode: number) => {
    Button(['紧凑(3列)', '适中(2列)', '宽大(1列)'][mode])
      .layoutWeight(1)
      .backgroundColor(this.cardSizeMode === mode ? '#2d5f8a' : '#e8ecf0')
      .fontColor(this.cardSizeMode === mode ? '#ffffff' : '#666')
      .onClick(() => { this.cardSizeMode = mode; })
  })
}

这种模式在电商应用的商品列表、图片库、文件管理器网格视图中非常常见,是实现「自适应列数」的推荐方案。

5.5 标签云(TagCloud)的另一种 Wrap 应用

@Component
struct TagCloud {
  private tags: string[] = [];
  @State selectedTag: string = '';

  build() {
    Flex() {
      ForEach(this.tags, (tag: string) => {
        Text(tag)
          .fontSize(13)
          .fontColor(tag === this.selectedTag ? '#ffffff' : '#2d5f8a')
          .backgroundColor(tag === this.selectedTag ? '#2d5f8a' : '#eef2f7')
          .padding({ left: 14, right: 14, top: 6, bottom: 6 })
          .borderRadius(20)
          .margin(4)
          .onClick(() => {
            this.selectedTag = this.selectedTag === tag ? '' : tag;
          })
      })
    }
    .flexDirection(FlexDirection.Row)
    .flexWrap(FlexWrap.Wrap)          // ★ 标签换行 ★
    .alignItems(ItemAlign.Center)
    .width('100%')
  }
}

标签云的特点是每个标签的宽度不同(取决于文字长度),使用 flexWrap(Wrap) 让它们自然换行,无需手动计算每行的标签组合。这也是 Wrap 最擅长的场景——内容宽度不固定的流式排列。


六、Flex + flexGrow:精准比例弹性增长

6.1 核心概念与公式

flexGrow 是弹性布局中最重要的单个属性。它定义了子组件在「剩余空间」中的分配比例。剩余空间 = 容器主轴尺寸 − 所有子组件内容尺寸之和(或 flexBasis 之和)。

分配公式

子项 N 的增长量 = 剩余空间 × (growN ÷ Σgrow)

其中 Σgrow 是所有非零 flexGrow 值的总和。

6.2 基础案例:单项填充所有剩余空间

Flex() {
  // A: flexGrow(0) → 不增长,只占内容宽度
  GrowBlock({ label: 'A', subLabel: 'grow(0)', grow: 0 })
  // B: flexGrow(0) → 不增长
  GrowBlock({ label: 'B', subLabel: 'grow(0)', grow: 0 })
  // C: flexGrow(1) → ★ 填充所有剩余空间 ★
  GrowBlock({ label: 'C', subLabel: 'grow(1) 填充', grow: 1 })
}
.flexDirection(FlexDirection.Row)
.width('100%').height(60)

在这个例子中,A 和 B 占据各自内容所需宽度后,容器的剩余宽度全部由 C 吸收。这就是弹性布局中最常见的「固定左 + 固定右 + 弹性中间」模式的实现原理。

6.3 比例分配:等差与不等差

1:1:1 — 三等分剩余空间

A(grow=1) : B(grow=1) : C(grow=1)
→ 每个占据剩余空间的 1/3

1:2:1 — 中间是两边的 2 倍

A(grow=1) : B(grow=2) : C(grow=1)
Σgrow = 4
A = 剩余空间 × 1/4
B = 剩余空间 × 2/4 = 剩余空间 × 1/2
C = 剩余空间 × 1/4

1:3:2 — 不等差分配

A(grow=1) : B(grow=3) : C(grow=2)
Σgrow = 6
A = 剩余空间 × 1/6
B = 剩余空间 × 3/6 = 剩余空间 × 1/2
C = 剩余空间 × 2/6 = 剩余空间 × 1/3

6.4 flexBasis + flexGrow 配合使用

flexBasis 设置子组件在「弹性分配前」的初始尺寸,flexGrow 在此之上叠加增长:

Flex() {
  // basis=80px, grow=0 → 固定 80px,不增长
  GrowBlock({ label: 'A:80px', grow: 0, basis: '80px' })
  // basis=120px, grow=1 → 从 120px 基础增长
  GrowBlock({ label: 'B:120px+', grow: 1, basis: '120px' })
  // basis=80px, grow=2 → 从 80px 基础翻倍增长
  GrowBlock({ label: 'C:80px++', grow: 2, basis: '80px' })
}
.flexDirection(FlexDirection.Row)
.width('100%')

计算过程

  1. A 固定 80px,B 初始 120px,C 初始 80px
  2. 已占用 = 80 + 120 + 80 = 280px + margins
  3. 剩余空间 = 容器宽度 − 280px − margins
  4. B 获得剩余空间的 1/(1+2) = 1/3
  5. C 获得剩余空间的 2/(1+2) = 2/3

6.5 垂直方向:页面骨架布局

Flex() {
  // 顶部:flexGrow(0) 固定高度 40px
  GrowBlock({ label: '顶部导航栏', grow: 0, basis: '40px', isColumn: true })
  // ★ 内容区:flexGrow(1) 弹性增长填充所有剩余高度 ★
  GrowBlock({ label: '★ 内容区 ★', grow: 1, isColumn: true })
  // 底部:flexGrow(0) 固定高度 40px
  GrowBlock({ label: '底部栏', grow: 0, basis: '40px', isColumn: true })
}
.flexDirection(FlexDirection.Column)    // ← 垂直方向
.height(240)                             // ← 容器固定高度
.width('100%')

这是移动端页面骨架的经典模式——头固定 + 内容区弹性 + 底固定。配合 flexDirection(Column)flexGrow(1) 让中间内容区自动填满头尾之外的所有剩余高度,适配从 iPhone SE 到折叠屏展开的任何屏幕高度。

6.6 交互式 flexGrow 演示

最有趣的部分是通过滑块实时调整 flexGrow 值,直观感受比例变化:

@Component
struct InteractiveGrowSection {
  @State growA: number = 1;
  @State growB: number = 1;
  @State growC: number = 1;

  build() {
    Column() {
      // 实时演示区
      Flex() {
        GrowBlock({ label: `A: grow(${this.growA.toFixed(1)})`,
          grow: this.growA, color: '#5B9BD5' })
        GrowBlock({ label: `B: grow(${this.growB.toFixed(1)})`,
          grow: this.growB, color: '#70AD47' })
        GrowBlock({ label: `C: grow(${this.growC.toFixed(1)})`,
          grow: this.growC, color: '#FFC000' })
      }
      .flexDirection(FlexDirection.Row).width('100%').height(56)

      // Slider 控制区
      GrowSlider({ label: '子项 A', color: '#5B9BD5', growValue: this.growA })
      GrowSlider({ label: '子项 B', color: '#70AD47', growValue: this.growB })
      GrowSlider({ label: '子项 C', color: '#FFC000', growValue: this.growC })

      // 比例摘要
      Text(`A:B:C = ${this.growA.toFixed(1)} : ${this.growB.toFixed(1)} : ${this.growC.toFixed(1)}`)
        .fontColor('#c7254e').fontWeight(FontWeight.Bold)
    }
  }
}

通过 Slider 将 growA 调为 0、growB 调为 0、growC 调为 1,观察 C 瞬间「膨胀」占满所有剩余空间——这就是 flexGrow 最直观的教学体验。


七、弹性布局的核心属性总结

7.1 属性速查表

属性 作用于 默认值 取值 类比 CSS
flexGrow 子组件 0 number(≥0) flex-grow
flexShrink 子组件 1 number(≥0) flex-shrink
flexBasis 子组件 'auto' number / string flex-basis
flexWrap 容器 FlexWrap.NoWrap NoWrap / Wrap / WrapReverse flex-wrap
justifyContent 容器 FlexAlign.Start 6 种对齐值 justify-content
alignItems 容器 ItemAlign.Stretch 4 种对齐值 align-items
alignContent 容器(多行) FlexAlign.Start 6 种对齐值 align-content
layoutWeight 子组件 0 number 无直接对应

7.2 flexGrow 与 layoutWeight 的对比

这两个属性最容易混淆。它们的根本区别在于分配什么:

flexGrow:
  [   内容宽度   | ←── 剩余空间 ──→    ]
  [ 子项A内容 ] [ 子项B 按 grow 分配 ]

layoutWeight:
  [ ←── 整个容器按 weight 重新分配 ──→ ]
  [ 按weight比例 ][ 按weight比例 ][ 按weight比例 ]

何时用 flexGrow:子项有「内容宽度」需要保留(如文字、图标),只需在内容之外弹性分配剩余空间。例如导航栏:左侧 LOGO(内容宽度)+ 中间搜索框(弹性)+ 右侧按钮(内容宽度)。

何时用 layoutWeight:子项之间需要严格的「等分」或「权重比例分配」,不考虑内容宽度。例如三等分按钮组、仪表盘面板中的等宽卡片。

7.3 常见的布局陷阱与解决方案

陷阱 1:flexGrow 不生效

原因:容器没有设置尺寸,或者容器尺寸刚好等于子项内容尺寸(无剩余空间)
解决:确保容器有明确的主轴尺寸,或者使用 layoutWeight 替代

陷阱 2:子项溢出容器

原因:flexShrink 默认是 1,但如果所有子项的 flexShrink 都是 0,它们不会缩小
解决:给需要压缩的子项设置 flexShrink(1),或者不要给内容固定的子项设 shrink(0)

陷阱 3:Wrap 不换行

原因:flexWrap 默认是 NoWrap
解决:显式设置 .flexWrap(FlexWrap.Wrap)

陷阱 4:ColumnReverse 下 justifyContent 方向混淆

误区:以为 Start 仍然是「顶部」
正确:ColumnReverse 下 Start = 底部,End = 顶部
记忆法:Start 永远指向主轴的「起始方向」,反向模式下起始方向变了

八、性能优化与最佳实践

8.1 布局性能建议

  1. 避免深层嵌套Flex 容器可以嵌套,但深度建议控制在 3-4 层以内。过深的布局树会让 ArkUI 的布局引擎在每次状态变化时产生更多的计算开销。

  2. 合理使用 layoutWeight:在卡片列表中使用 layoutWeight 比逐个计算百分比宽度更高效,因为 layoutWeight 的分配算法在框架层是高度优化的。

  3. @State 的作用域@State 变量的变化会触发其所在组件的 build() 重渲染,尽量将状态作用域限制在真正需要更新的组件层级,避免父组件的无关子组件被连带重渲染。

  4. 懒加载长列表:当商品卡片超过 20 项时,应考虑使用 LazyForEach 替代 ForEach,实现按需渲染:

LazyForEach(this.productDataSource, (item: ProductItem) => {
  ProductCard({ product: item, cardSizeMode: this.cardSizeMode })
})

8.2 组件化设计模式

从本文的演示代码可以看出,我们采用了清晰的组件化分层:

Page(页面容器)
  ├── SectionCard(演示区块容器,接收 @BuilderParam content)
  │   ├── FlexContent1 / FlexContent2 / ...(具体演示内容)
  │   └── CodeBlock(代码高亮模块)
  ├── InteractiveGrowSection(交互式区域)
  └── FlexGrowTechPanel(技术说明面板)

这种设计模式的优势:

  • 关注点分离:页面结构、区块容器、具体演示内容各司其职
  • 复用性SectionCard 可以复用于所有演示区块,CodeBlock 可以复用于所有代码展示
  • 可测试性:每个子组件可以独立测试和调试
  • 可维护性:修改某一个演示内容不会影响其他区域

8.3 主题化与视觉规范

在多个演示页面中,我们使用了统一的视觉规范:

元素 规范
页面背景 浅灰 #eef2f7
标题栏背景 深蓝 #2d5f8a
卡片背景 白色 #ffffff
区块标题 深色粗体 #1a1a2e
描述文字 灰色 #666666
代码高亮 红色粗体 #c7254e
代码正文 蓝色 #2d5f8a
卡片阴影 半透明 rgba(0,0,0,N%)
圆角 标准 8px / 12px / 14px

这种一致性不仅提升了视觉质量,也让代码中的颜色常量更容易维护——如果未来需要更换主题色,只需修改常量的定义。


九、结语:弹性布局的设计思维

Column + justifyContent(Start) 的基础对齐,到 Flex + flexDirection 的四种方向模式,再到 Flex + Wrap + layoutWeight 的响应式卡片流,最后到 flexGrow 的精确比例控制——我们看到了一条清晰的学习路径。

弹性布局的核心理念可以归纳为三句话:

  1. 永远不要用固定尺寸。能用 flexGrowlayoutWeightflexBasis 的地方,就不要写 width(100)height(200)

  2. 拥抱「剩余空间」的思维。弹性布局的本质是对容器剩余空间的分配。理解了这个概念,flexGrowflexShrink 就不再是神秘的魔法数字。

  3. 用组合而非嵌套解决问题。一个带 flexWrapFlex 容器加上 layoutWeight 的子组件,能解决绝大多数响应式排列需求,远比多层嵌套的 Column + Row 组合更简洁、更高效。

HarmonyOS NEXT 的 ArkUI 框架为开发者提供了一套现代化、完善的弹性布局体系。掌握这些布局模式后,再去构建应用界面,你会发现——布局不再是与屏幕尺寸的博弈,而是对空间优雅的分配


附录:完整项目文件结构

MyApplication/
├── AppScope/
│   ├── app.json5                        # 应用全局配置
│   └── resources/                       # 应用级资源
├── entry/
│   └── src/main/ets/
│       ├── entryability/
│       │   └── EntryAbility.ets          # 应用入口
│       └── pages/
│           ├── Index.ets                 # Column + justifyContent 演示
│           └── FlexGrowDemo.ets          # Flex + flexGrow 弹性增长演示
├── build-profile.json5                  # 构建配置(SDK 24)
└── hvigorfile.ts                        # Hvigor 构建脚本
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐