鸿蒙从零到一 ArkUI 常用布局实战:Column、Row、Stack 与 Flex
前言
ArkUI 使用声明式语法描述界面,组件树不仅决定页面展示什么,也决定每个组件如何获得尺寸、如何排列以及如何响应屏幕变化。初学者常见的问题不是组件不会写,而是多个组件放到一起后出现挤压、溢出、错位,或者换一台设备就失去原有结构。
本文围绕 Column、Row、Stack 和 Flex 四类常用布局展开,从主轴与交叉轴讲起,逐步完成一个可适配不同宽度的个人资料页。示例使用 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 获得剩余空间。配合 maxLines 与 textOverflow,长文本不会把按钮挤出屏幕。
四、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,优先使用 alignContent、margin 或相对尺寸。必须精确定位时,也要确保 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、文本溢出控制和栅格断点,就能覆盖常见应用页面。
开发复杂界面时,可以先用少量容器搭出结构,再逐步补充尺寸、间距和视觉样式。保持每层布局职责单一,后续适配不同设备、调整内容和排查错位都会更轻松。