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

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

前言

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

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

一、先理解 ArkUI 的布局过程

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

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

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

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

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

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

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

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

arkts 复制代码
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 分配主轴空间:

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

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

三、Row:构建水平操作区

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

arkts 复制代码
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

arkts 复制代码
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 会让子组件叠放在同一区域,后声明的子组件通常显示在更上层。它适合图片角标、遮罩、浮动按钮和空状态覆盖层。

arkts 复制代码
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 指定位置:

arkts 复制代码
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 都能沿方向排列元素,但它提供更完整的弹性布局能力,尤其适合数量不固定的标签和需要自动换行的操作项。

arkts 复制代码
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 更合适。

六、组合布局完成资料页

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

arkts 复制代码
@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 让同级组件合理共享剩余空间。

七、让布局适应不同宽度

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

优先约束关键尺寸

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

arkts 复制代码
Row({ space: 12 }) {
  Image($r('app.media.app_icon'))
    .width(48)
    .height(48)

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

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

使用 GridRow 切换栏数

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

arkts 复制代码
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 写死高度,因为文字大小、系统字体缩放和横竖屏都会改变内容所需空间。

九、布局选择速查

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

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

总结

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

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

相关推荐
海上彼尚2 小时前
Nodejs也能写Agent - 17.LangGraph篇 - Checkpointer 与 Memory
前端·javascript·人工智能·langchain·node.js
盛夏绽放2 小时前
Hermes Agent 全栈安装、配置与Web端架构深度解析(官方命令版·最终优化)
前端·架构·hermes
早期的虫儿有鸟吃2 小时前
Vue3
前端·javascript·vue.js
hqk2 小时前
我给 AI 造了一个原型工具:AIWireframeViewer V1
前端·ai编程·vibecoding
Highcharts.js2 小时前
商业授权与开源图表库如何选择?
前端·javascript·开源·highcharts·chart.js·echarts.js
工具派2 小时前
写代码时,我把这个本地 AI Commit Message 生成器接进了工作流
前端·后端
LaughingZhu3 小时前
Product Hunt 每日热榜 | 2026-07-15
前端·人工智能·神经网络·react.js·搜索引擎·前端框架
掘金者阿豪3 小时前
从Oracle迁到金仓,第一个月踩的最大的坑就是它
前端·后端
AZaLEan__3 小时前
CSS文档流相关:BFC
前端·css