[鸿蒙从零到一] ArkUI 常用布局实战:Column、Row、Stack 与 Flex

5 阅读7分钟

[鸿蒙从零到一] ArkUI 常用布局实战:Column、Row、Stack 与 Flex

前言

ArkUI 使用声明式语法描述界面,组件树不仅决定页面展示什么,也决定每个组件如何获得尺寸、如何排列以及如何响应屏幕变化。初学者常见的问题不是组件不会写,而是多个组件放到一起后出现挤压、溢出、错位,或者换一台设备就失去原有结构。

本文围绕 ColumnRowStackFlex 四类常用布局展开,从主轴与交叉轴讲起,逐步完成一个可适配不同宽度的个人资料页。示例使用 ArkTS 与 ArkUI 声明式语法,适合 Stage 模型工程中的页面和自定义组件。

一、先理解 ArkUI 的布局过程

一个组件最终能显示在哪里,通常会经历约束、测量和放置几个环节:

  • 父组件向子组件传递可用空间和尺寸约束。
  • 子组件根据内容、显式尺寸与约束计算自身大小。
  • 父组件按照布局规则确定子组件的位置。
  • 绘制阶段再处理背景、边框、阴影和内容。

因此,.width('100%') 并不代表占满整个屏幕,而是尽量占满父组件允许的宽度。父组件尺寸不明确时,百分比尺寸也可能得不到预期结果。

下面先准备一个基础页面:

@Entry
@Component
struct LayoutDemoPage {
  build() {
    Column() {
      Text('布局实战')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#F5F7FA')
  }
}

根布局显式占满可用空间,后续子组件的百分比宽度和对齐规则才有稳定参照。

二、Column:沿垂直方向组织内容

Column 默认从上到下排列子组件,适合页面骨架、表单和纵向信息区。它的主轴是垂直方向,交叉轴是水平方向。

Column({ space: 12 }) {
  Text('Lulu')
    .fontSize(22)
    .fontWeight(FontWeight.Bold)

  Text('HarmonyOS 应用开发者')
    .fontSize(14)
    .fontColor('#667085')

  Button('编辑资料')
}
.width('100%')
.padding(20)
.alignItems(HorizontalAlign.Start)
.backgroundColor(Color.White)
.borderRadius(8)

space 用于设置相邻子组件之间的固定间距,比给每个组件分别添加 margin 更简洁。alignItems(HorizontalAlign.Start) 控制子组件在水平方向靠左对齐。

主轴对齐与交叉轴对齐

Column 拥有多余的垂直空间时,可以通过 justifyContent 分配主轴空间:

Column() {
  Text('顶部内容')
  Text('底部内容')
}
.height(180)
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(HorizontalAlign.Center)

这里两个文本会分别贴近容器顶部和底部,并在水平方向居中。要让 justifyContent 的效果可见,容器必须有可分配的剩余空间。

三、Row:构建水平操作区

Row 从左到右排列子组件,主轴是水平方向,交叉轴是垂直方向。头像与昵称、标题与操作按钮、工具栏都适合使用它。

Row({ space: 12 }) {
  Image($r('app.media.app_icon'))
    .width(56)
    .height(56)
    .borderRadius(28)

  Column({ space: 4 }) {
    Text('Lulu')
      .fontSize(18)
      .fontWeight(FontWeight.Medium)

    Text('专注 ArkTS 与 ArkUI')
      .fontSize(13)
      .fontColor('#667085')
  }
  .alignItems(HorizontalAlign.Start)

  Blank()

  Button('关注')
    .height(36)
}
.width('100%')
.padding(16)
.alignItems(VerticalAlign.Center)
.backgroundColor(Color.White)

Blank 会吸收主轴上的剩余空间,把右侧按钮推到末端。它适合明确的两端式结构,但不要在复杂页面中堆叠多个 Blank,否则空间分配会变得难以理解。

使用 layoutWeight 分配剩余空间

当中间文本需要占据剩余宽度时,可以使用 layoutWeight

Row({ space: 12 }) {
  Image($r('app.media.app_icon'))
    .width(48)
    .height(48)
    .borderRadius(24)

  Column({ space: 4 }) {
    Text('这是一个可能很长的用户名称')
      .fontSize(16)
      .maxLines(1)
      .textOverflow({ overflow: TextOverflow.Ellipsis })

    Text('个人简介同样需要避免挤压右侧操作按钮')
      .fontSize(13)
      .fontColor('#667085')
      .maxLines(1)
      .textOverflow({ overflow: TextOverflow.Ellipsis })
  }
  .layoutWeight(1)
  .alignItems(HorizontalAlign.Start)

  Button('更多')
    .height(36)
}
.width('100%')

固定宽度的头像和按钮完成测量后,中间 Column 获得剩余空间。配合 maxLinestextOverflow,长文本不会把按钮挤出屏幕。

四、Stack:处理叠放与覆盖

Stack 会让子组件叠放在同一区域,后声明的子组件通常显示在更上层。它适合图片角标、遮罩、浮动按钮和空状态覆盖层。

Stack({ alignContent: Alignment.BottomEnd }) {
  Image($r('app.media.app_icon'))
    .width('100%')
    .height(180)
    .objectFit(ImageFit.Cover)

  Text('推荐')
    .fontSize(12)
    .fontColor(Color.White)
    .padding({ left: 10, right: 10, top: 6, bottom: 6 })
    .margin(12)
    .backgroundColor('#D92D20')
    .borderRadius(4)
}
.width('100%')
.height(180)
.clip(true)
.borderRadius(8)

alignContent 决定未单独定位的子组件在容器中的对齐位置。示例将角标放在右下角,并通过 margin 留出边缘距离。

绝对定位要谨慎使用

Stack 中可以通过 position 指定位置:

Stack() {
  Text('封面区域')
  Button({ type: ButtonType.Circle }) {
    Text('+').fontSize(24)
  }
  .width(44)
  .height(44)
  .position({ x: 16, y: 96 })
}
.width('100%')
.height(156)

绝对坐标不会自动理解不同屏幕、字体缩放和内容变化。适合位置稳定的小范围装饰或浮层,不适合承担页面主体排版。主体布局优先使用 Row、Column、Flex 和约束关系。

五、Flex:在空间不足时自动换行

Flex 与 Row、Column 都能沿方向排列元素,但它提供更完整的弹性布局能力,尤其适合数量不固定的标签和需要自动换行的操作项。

Flex({
  direction: FlexDirection.Row,
  wrap: FlexWrap.Wrap,
  justifyContent: FlexAlign.Start,
  alignItems: ItemAlign.Center,
  alignContent: FlexAlign.Start
}) {
  ForEach(['ArkTS', 'ArkUI', 'Stage 模型', '性能优化', '应用架构'], (tag: string) => {
    Text(tag)
      .fontSize(13)
      .fontColor('#344054')
      .padding({ left: 12, right: 12, top: 7, bottom: 7 })
      .margin({ right: 8, bottom: 8 })
      .backgroundColor('#EEF2F6')
      .borderRadius(4)
  }, (tag: string) => tag)
}
.width('100%')

当一行剩余宽度不足以容纳下一个标签时,FlexWrap.Wrap 会将其放到下一行。这里要区分两个属性:

  • alignItems 控制一行内部的元素如何沿交叉轴对齐。
  • alignContent 控制存在多行时,各行如何分配交叉轴空间。

如果元素数量固定、无需换行,使用 Row 或 Column 通常更直观;如果元素需要伸缩、换行或复杂空间分配,Flex 更合适。

六、组合布局完成资料页

实际页面通常不是单一布局,而是多种容器按职责组合。下面将头像信息、数据统计、标签和操作按钮组织成一个完整页面。

@Entry
@Component
struct ProfilePage {
  private skills: string[] = [
    'ArkTS', 'ArkUI', 'Stage 模型', '性能优化', '应用架构'
  ]

  build() {
    Scroll() {
      Column({ space: 16 }) {
        this.ProfileHeader()
        this.Statistics()
        this.SkillPanel()
        this.ActionBar()
      }
      .width('100%')
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F7FA')
  }

  @Builder
  ProfileHeader() {
    Row({ space: 12 }) {
      Stack({ alignContent: Alignment.BottomEnd }) {
        Image($r('app.media.app_icon'))
          .width(64)
          .height(64)
          .borderRadius(32)

        Circle()
          .width(14)
          .height(14)
          .fill('#12B76A')
          .stroke(Color.White)
          .strokeWidth(2)
      }
      .width(64)
      .height(64)

      Column({ space: 5 }) {
        Text('Lulu')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)

        Text('HarmonyOS 应用开发者')
          .fontSize(14)
          .fontColor('#667085')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding(20)
    .alignItems(VerticalAlign.Center)
    .backgroundColor(Color.White)
    .borderRadius(8)
  }

  @Builder
  Statistics() {
    Row() {
      this.StatItem('文章', '36')
      Divider().vertical(true).height(32).color('#EAECF0')
      this.StatItem('关注', '128')
      Divider().vertical(true).height(32).color('#EAECF0')
      this.StatItem('收藏', '2.4k')
    }
    .width('100%')
    .padding({ top: 16, bottom: 16 })
    .backgroundColor(Color.White)
    .borderRadius(8)
  }

  @Builder
  StatItem(label: string, value: string) {
    Column({ space: 4 }) {
      Text(value).fontSize(18).fontWeight(FontWeight.Bold)
      Text(label).fontSize(13).fontColor('#667085')
    }
    .layoutWeight(1)
  }

  @Builder
  SkillPanel() {
    Column({ space: 12 }) {
      Text('技术方向')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .width('100%')

      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(this.skills, (skill: string) => {
          Text(skill)
            .fontSize(13)
            .padding({ left: 12, right: 12, top: 7, bottom: 7 })
            .margin({ right: 8, bottom: 8 })
            .backgroundColor('#EEF2F6')
            .borderRadius(4)
        }, (skill: string) => skill)
      }
      .width('100%')
    }
    .width('100%')
    .padding(20)
    .alignItems(HorizontalAlign.Start)
    .backgroundColor(Color.White)
    .borderRadius(8)
  }

  @Builder
  ActionBar() {
    Row({ space: 12 }) {
      Button('发消息')
        .layoutWeight(1)
        .type(ButtonType.Normal)
        .borderRadius(6)

      Button('关注')
        .layoutWeight(1)
        .type(ButtonType.Normal)
        .borderRadius(6)
    }
    .width('100%')
  }
}

这个页面的职责划分很清楚:

  • 外层 Scroll 处理小屏或横屏下内容超过视口的情况。
  • Column 负责纵向组织各功能区。
  • Row 负责头像信息、统计数据和操作按钮的水平排列。
  • Stack 将在线状态叠加到头像上。
  • Flex 让技术标签根据可用宽度自动换行。
  • layoutWeight 让同级组件合理共享剩余空间。

七、让布局适应不同宽度

响应式布局的核心不是为每一种设备写一套页面,而是减少不必要的固定尺寸,让组件根据父容器约束调整。

优先约束关键尺寸

头像、图标、触控按钮通常需要稳定尺寸;文本区、卡片和列表更适合使用百分比宽度或剩余空间。

Row({ space: 12 }) {
  Image($r('app.media.app_icon'))
    .width(48)
    .height(48)

  Text('可伸缩的正文区域')
    .layoutWeight(1)

  Button('操作')
    .height(40)
}
.width('100%')

使用 GridRow 切换栏数

当页面需要在不同宽度下改变结构时,可以使用栅格断点能力:

GridRow({
  columns: { sm: 4, md: 8, lg: 12 },
  gutter: { x: 12, y: 12 }
}) {
  GridCol({ span: { sm: 4, md: 4, lg: 6 } }) {
    Text('资料概览')
      .width('100%')
      .height(120)
      .backgroundColor(Color.White)
  }

  GridCol({ span: { sm: 4, md: 4, lg: 6 } }) {
    Text('近期动态')
      .width('100%')
      .height(120)
      .backgroundColor(Color.White)
  }
}
.width('100%')

窄屏下每个区域占满一行,中等及更宽屏幕下两个区域并排。相比读取屏幕像素后手工计算宽度,栅格布局更容易维护。

八、常见问题与排查方法

百分比宽度没有生效

先检查父组件是否有明确宽度。子组件的百分比以父组件提供的约束为基础,不能脱离父布局单独理解。

Row 中的文本把按钮挤出屏幕

给文本所在容器设置 .layoutWeight(1),并限制文本行数与溢出方式。固定尺寸组件保留自身宽度,弹性区域使用剩余空间。

justifyContent 看不出变化

通常是容器没有剩余空间。为容器设置明确高度或宽度后,再观察主轴分布效果。

Stack 覆盖层位置在不同设备上漂移

减少基于绝对坐标的 position,优先使用 alignContentmargin 或相对尺寸。必须精确定位时,也要确保 Stack 自身尺寸稳定。

页面内容在小屏幕被截断

如果页面整体高度可能超过视口,使用 Scroll 包裹纵向内容。不要简单给根 Column 写死高度,因为文字大小、系统字体缩放和横竖屏都会改变内容所需空间。

九、布局选择速查

需求推荐布局关键属性
垂直页面骨架、表单Columnspace、justifyContent、alignItems
水平工具栏、图文信息Rowspace、layoutWeight、Blank
角标、遮罩、悬浮元素StackalignContent、position
标签流、自动换行Flexdirection、wrap、alignContent
跨宽度改变栏数GridRow / GridColcolumns、span、gutter

选择布局时,先问清楚三个问题:内容沿哪个方向排列,空间不足时是否需要换行,元素之间是顺序关系还是覆盖关系。答案明确后,容器选择通常也就明确了。

总结

ArkUI 布局的关键不在于记住多少属性,而在于理解父子约束、主轴、交叉轴和剩余空间。Column 与 Row 负责大多数顺序排版,Stack 解决叠放关系,Flex 处理弹性与换行,再配合 layoutWeight、文本溢出控制和栅格断点,就能覆盖常见应用页面。

开发复杂界面时,可以先用少量容器搭出结构,再逐步补充尺寸、间距和视觉样式。保持每层布局职责单一,后续适配不同设备、调整内容和排查错位都会更轻松。