【共创季稿事节】鸿蒙 Next ArkTS 布局精讲:Flex 主轴对齐 justifyContent 五种模式完整指南

鸿蒙 Next ArkTS 布局精讲:Flex 主轴对齐 justifyContent 五种模式完整指南


一、前言

在鸿蒙 Next 应用开发中,布局是一切 UI 的基石。justifyContent 属性控制着 Flex 容器内子项在主轴方向上的排列策略,是写出"看着舒服、逻辑清晰"界面的关键。本文将从概念到代码落地,结合一个可直接运行的实战案例,让你一次看懂全部六种对齐模式。


二、Flex 布局基础

2.1 什么是 Flex 布局

Flex(弹性布局)是鸿蒙 ArkUI 中最核心的布局容器之一。与 Row / Column 相比,Flex 提供了更丰富的子项排列控制:

  • 主轴方向(direction):Row(水平)或 Column(垂直)
  • 主轴对齐(justifyContent):子项在主轴上的分布方式
  • 交叉轴对齐(alignItems):子项在交叉轴上的对齐方式
  • 换行(wrap):是否允许子项折行

2.2 主轴与交叉轴

理解 justifyContent 必须先理解"主轴":当 direction: Row 时主轴为水平方向(从左到右);当 direction: Column 时主轴为垂直方向(从上到下)。justifyContent 始终作用于主轴方向。


三、justifyContent 六种模式详解

3.1 FlexAlign.Start(默认值)

所有子项从主轴的起始位置开始紧密排列,尾部留出空白。

复制代码
水平: [■ ■ ■ ■ ■]···········

适用场景: 导航栏左侧菜单项、表单标签前的图标列表、"左对齐"布局需求。

3.2 FlexAlign.Center

所有子项整体在主轴居中,两侧空白均匀。

复制代码
水平: ·····[■ ■ ■ ■ ■]·····

适用场景: 模态框按钮组、页面正中的功能图标、需要视觉对称的场景。

3.3 FlexAlign.End

所有子项从主轴结束位置开始紧密排列,首部留出空白。

复制代码
水平: ·······[■ ■ ■ ■ ■]

适用场景: 页面右下角的浮动操作按钮、聊天框右侧的发送按钮组。

3.4 FlexAlign.SpaceBetween

首子项贴起点,末子项贴终点,剩余空间均匀分布在子项之间。

复制代码
水平: [■]·······[■]·······[■]·······[■]·······[■]

适用场景: 底部导航栏 Tab、商品列表三栏分布。"首尾贴边、中间均分"。

至少 2 个子项效果才明显,单子项时等同于 Start。

3.5 FlexAlign.SpaceAround

每个子项两侧空间相等。首尾子项外侧空间是中间子项两侧空间的一半。

复制代码
水平: ··[■]··[■]··[■]··[■]··[■]··

公式: 设容器宽 W,子项宽和 S,子项数 N,首尾间隙 G,中间间隙 2G,则 W = S + 2N*G,得 G = (W-S)/(2N)

适用场景: 勋章标签展示、功能图标网格。

3.6 FlexAlign.SpaceEvenly

所有间距(含首尾)完全相等,最"数学对称"的方式。

复制代码
水平: ····[■]····[■]····[■]····[■]····[■]····

公式: W = S + (N+1)*G,得 G = (W-S)/(N+1)

适用场景: 底部操作栏等距图标、分页指示器圆点排列。

3.7 三者对比速查

模式 首尾间隙 中间间隙 公式 特点
SpaceBetween 0 均分剩余 (W-S)/(N-1) 首尾贴边
SpaceAround G 2G (W-S)/(2N) 两侧等宽
SpaceEverly G G (W-S)/(N+1) 完全等距

四、完整代码实战

下面给出完整可运行示例,项目结构如下:

复制代码
entry/src/main/ets/pages/
├── Index.ets                  # 导航首页
└── FlexJustifyContentDemo.ets # 主轴对齐演示页

4.1 导航首页(Index.ets)

typescript 复制代码
import { router } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  build() {
    Column({ space: 20 }) {
      Text('鸿蒙 ArkTS 布局示例')
        .fontSize(24).fontWeight(FontWeight.Bold).margin({ top: 40 });
      Text('点击下方卡片体验布局效果').fontSize(14).fontColor('#888888');

      Column({ space: 12 }) {
        Row({ space: 8 }) {
          Text('≡').fontSize(28).fontWeight(FontWeight.Bold).fontColor('#007AFF');
          Column({ space: 4 }) {
            Text('Flex 主轴对齐').fontSize(18).fontWeight(FontWeight.Medium);
            Text('justifyContent 五种模式 · Row / Column 方向切换')
              .fontSize(12).fontColor('#888888');
          }.alignItems(HorizontalAlign.Start);
        }.width('100%');
        Text('Start | Center | End | SpaceBetween | SpaceAround | SpaceEvenly')
          .fontSize(11).fontColor('#007AFF').width('100%');
      }
      .width('90%').padding(16).backgroundColor('#FFFFFF').borderRadius(12)
      .shadow({ radius: 6, color: '#30000000', offsetX: 0, offsetY: 3 })
      .onClick(() => { router.pushUrl({ url: 'pages/FlexJustifyContentDemo' }); })

      Text('点击上方卡片进入演示').fontSize(12).fontColor('#CCCCCC').margin({ top: 20 });
    }.width('100%').height('100%').backgroundColor('#F5F5F5')
    .alignItems(HorizontalAlign.Center);
  }
}

4.2 核心演示页(FlexJustifyContentDemo.ets)

typescript 复制代码
interface JustifyItem { align: FlexAlign; label: string; desc: string; }
interface DirectionOption { value: FlexDirection; label: string; }

@Entry
@Component
struct FlexJustifyContentDemo {
  @State private currentDirection: FlexDirection = FlexDirection.Row;

  private readonly directionOptions: DirectionOption[] = [
    { value: FlexDirection.Row, label: '水平主轴 (Row)' },
    { value: FlexDirection.Column, label: '垂直主轴 (Column)' },
  ];

  private readonly justifyList: JustifyItem[] = [
    { align: FlexAlign.Start, label: 'FlexAlign.Start',
      desc: '子项从主轴起点开始紧密排列' },
    { align: FlexAlign.Center, label: 'FlexAlign.Center',
      desc: '子项在主轴居中对齐' },
    { align: FlexAlign.End, label: 'FlexAlign.End',
      desc: '子项从主轴终点开始紧密排列' },
    { align: FlexAlign.SpaceBetween, label: 'FlexAlign.SpaceBetween',
      desc: '首尾子项贴边,剩余空间均匀分布在子项之间' },
    { align: FlexAlign.SpaceAround, label: 'FlexAlign.SpaceAround',
      desc: '每个子项两侧空间相等(首尾空间为中间的一半)' },
    { align: FlexAlign.SpaceEvenly, label: 'FlexAlign.SpaceEvenly',
      desc: '所有间距(含首尾)完全相等' },
  ];

  private readonly colorList: ResourceColor[] = [
    '#FF6B81', '#5B8FF9', '#5AD8A6', '#F6BD16', '#E8684A',
  ];

  build() {
    Scroll() {
      Column({ space: 16 }) {
        Text('Flex 主轴对齐 · justifyContent').fontSize(22)
          .fontWeight(FontWeight.Bold).width('100%')
          .textAlign(TextAlign.Center).padding({ top: 20, bottom: 8 });

        Text('FlexAlign.Start / Center / End / SpaceBetween / SpaceAround / SpaceEvenly')
          .fontSize(13).fontColor('#666666').width('100%')
          .textAlign(TextAlign.Center).padding({ bottom: 8 });

        Text('切换主轴方向:').fontSize(14)
          .fontWeight(FontWeight.Medium).width('100%').padding({ left: 16 });

        Row({ space: 12 }) {
          ForEach(this.directionOptions, (item: DirectionOption) => {
            Button(item.label).fontSize(14).height(36).borderRadius(18)
              .backgroundColor(this.currentDirection === item.value ? '#007AFF' : '#E8E8E8')
              .fontColor(this.currentDirection === item.value ? '#FFFFFF' : '#333333')
              .onClick(() => { this.currentDirection = item.value; });
          });
        }.width('100%').padding({ left: 16, right: 16, bottom: 4 });

        ForEach(this.justifyList, (item: JustifyItem) => { this.justifyCard(item); });
      }.width('100%').padding({ bottom: 32 })
    }.width('100%').height('100%').backgroundColor('#F5F5F5')
  }

  @Builder
  justifyCard(item: JustifyItem) {
    Column({ space: 6 }) {
      Text(item.label).fontSize(15).fontWeight(FontWeight.Medium)
        .fontColor('#222222').width('100%');
      Text(item.desc).fontSize(12).fontColor('#888888').width('100%');

      // ★ 核心:Flex 容器 + justifyContent
      Flex({ direction: this.currentDirection, justifyContent: item.align }) {
        ForEach(this.colorList, (color: ResourceColor) => {
          Stack() { /* 编号由子级自行填充 */ }
            .width(this.currentDirection === FlexDirection.Row ? 48 : '100%')
            .height(this.currentDirection === FlexDirection.Row ? 48 : 36)
            .backgroundColor(color).borderRadius(6)
            .margin({ left: 1, right: 1, top: 1, bottom: 1 });
        });
      }
      .width('100%')
      .height(this.currentDirection === FlexDirection.Row ? 60 : 220)
      .padding(4).backgroundColor('#FFFFFF').borderRadius(8)
      .border({ width: 1, color: '#E0E0E0' })
      .shadow({ radius: 4, color: '#20000000', offsetX: 0, offsetY: 2 })
    }.width('100%').padding({ left: 16, right: 16 })
  }
}

4.3 路由注册

修改 entry/src/main/resources/base/profile/main_pages.json

json 复制代码
{ "src": [ "pages/Index", "pages/FlexJustifyContentDemo" ] }

五、ArkTS 语法避坑指南

5.1 Object literals as types

错误: Object literals cannot be used as type declarations

原因: ArkTS 禁止内联对象字面量作为类型声明。

解决: 定义显式 interface

typescript 复制代码
// ❌ 错误
private readonly options: { value: FlexDirection; label: string }[] = [...]

// ✅ 正确
interface DirectionOption { value: FlexDirection; label: string; }
private readonly options: DirectionOption[] = [...]

5.2 FlexDirection/FlexAlign 导入问题

错误: Module '@kit.ArkUI' has no exported member 'FlexDirection'

原因: 这两个枚举是 ArkUI 全局类型 ,无需 import。

解决: 删除 import { FlexDirection, FlexAlign } from '@kit.ArkUI'; 即可。

5.3 overlay 传参

错误: Argument of type 'TextAttribute' is not assignable to parameter

原因: .overlay() 不接受直接传 Text(...),它需要 string | CustomBuilder

解决:Stack 包裹内容替代 overlay。

typescript 复制代码
// ❌ 错误
Column().overlay(Text('1').fontSize(16));

// ✅ 正确
Stack() { Text('1').fontSize(16); }.width(48).height(48).backgroundColor(color);

5.4 其他常见错误

错误信息 原因 解决
Array literals not inferrable 数组元素类型无法推断 给变量添加显式类型标注
No exported member 导入了框架已移除的 API 使用 Kit 化新路径 @kit.ArkUI

六、API 24 新特性与变化

6.1 Kit 化导入路径

API 24 将 API 按功能域归入 Kit 包:

旧方式(API 9~11) 新方式(API 24)
import router from '@ohos.router' import { router } from '@kit.ArkUI'
import window from '@ohos.window' import { window } from '@kit.ArkUI'
import hilog from '@ohos.hilog' import { hilog } from '@kit.PerformanceAnalysisKit'

6.2 @Builder 传参改进

API 24 中 @Builder 支持更灵活的传参方式,包括对象参数、可选参数和默认值。本示例中 justifyCard(item: JustifyItem) 即是典型用法。

6.3 Scroll + Column vs List

当内容量不大、无需复用机制时,Scroll() + Column()List() 更轻量、更直观。本示例采用了这种组合。


七、最佳实践

7.1 对齐模式选型建议

场景 推荐模式 原因
表单操作按钮(确定/取消) Center 视觉对称
顶部导航菜单项 Start 左上角阅读起点
底部工具栏图标 SpaceEvenly 各图标权重相同
卡片标签列表 SpaceAround 视觉呼吸感更好
底部 Tab 导航(2-4 项) SpaceBetween 充分利用屏幕宽度
聊天消息时间戳 End 右侧对齐

7.2 结合 alignContent 实现二维对齐

当 Flex 容器换行时,alignContent 控制多行在交叉轴上的分布,与 justifyContent 配合可实现完整二维弹性布局:

typescript 复制代码
Flex({
  direction: FlexDirection.Row,
  wrap: FlexWrap.Wrap,
  justifyContent: FlexAlign.SpaceEvenly,
  alignContent: FlexAlign.SpaceBetween,
}) { /* 子项先水平等距分布,多行之间均匀填充 */ }

7.3 常见布局问题排查

问题 可能原因 解决
子项未按预期对齐 direction 设置错误 确认主轴方向
SpaceBetween 无效果 只有 1 个子项 至少 2 个子项
子项溢出容器 子项总宽 > 容器宽 添加 wrap 或减小子项尺寸
切换方向后布局乱 固定宽高未适配 用三元表达式动态适配

八、扩展练习

挑战一:嵌套 Flex --- 外层 SpaceBetween 分配三个大区,每个大区内使用不同 justifyContent 模式。

挑战二:动画切换 --- 为方向按钮添加 .animation({ duration: 300, curve: Curve.EaseInOut })

挑战三:响应式自适应 --- 让容器宽度随窗口变化验证各模式的稳定性。


九、总结

本文从 Flex 布局基础出发,深入讲解了 justifyContent 六种对齐模式及其数学原理,给出了完整可运行示例,并覆盖了 ArkTS 语法避坑、API 24 新特性和最佳实践。

核心要点:

  1. justifyContent 作用于主轴方向 ,主轴由 FlexDirection 决定
  2. 六种模式分三类:边界对齐(Start/Center/End)、空间填充(SpaceBetween)、均匀分布(SpaceAround/SpaceEvenly)
  3. SpaceAround 与 SpaceEvenly 的区别:前者首尾间隙是中间的一半,后者所有间距相等
  4. ArkTS 严格模式 下需用显式 interface 替代内联对象字面量,FlexDirection/FlexAlign 为全局类型无需 import
  5. API 24 推荐 Kit 化导入路径(@kit.ArkUI),@Builder 装饰器更灵活

本文配套代码在项目 entry/src/main/ets/pages/ 目录下,可直接在 DevEco Studio 中打开运行。