【共创季稿事节】鸿蒙原生 ArkTS 布局深度解析:Stack 容器与 ZOrder 层级控制实战
鸿蒙原生 ArkTS 布局深度解析:Stack 容器与 ZOrder 层级控制实战



一、引言
HarmonyOS NEXT 作为华为自研的全场景分布式操作系统,从底层彻底剥离了 AOSP 代码,实现了"纯鸿蒙"的生态闭环。随之而来的 ArkTS(Ark TypeScript)语言也演进到了 3.0 版本,成为构建鸿蒙原生应用的核心语言。
在 ArkUI 声明式 UI 框架中,布局系统是最基础也最重要的能力之一。与前端领域的 CSS Flexbox、Grid 类似,ArkUI 提供了一系列布局容器组件来帮助开发者组织界面元素。其中,Stack 容器 是一种"自由层叠式"布局容器,允许子组件在 Z 轴方向上叠放组合,是实现复杂 UI 效果(悬浮按钮、模态弹窗、卡片叠加、游戏 UI)的利器。
然而,许多开发者在使用 Stack 时,常常困惑于两个问题:
- 各个子组件之间的叠放顺序是如何确定的?
- 如何精确控制某个子组件浮在另一组件的上方或下方?
答案是:zIndex 属性(又名 ZOrder)。
本文将通过一个完整的可运行 Demo,带您彻底搞懂 Stack 容器的叠放机制,以及如何用 .zIndex() 精确控制子组件的层级顺序。
二、Stack 容器基础概念
2.1 什么是 Stack?
Stack 是 ArkUI 提供的一种层叠布局容器。它的核心行为是:
所有子组件默认从 Stack 容器的左上角 (0, 0) 开始,按添加顺序依次向上叠加。
这句话包含了两个关键信息:
- 位置起点:每个子组件若不通过
.position()或.align()指定偏移,则都从左上角原点开始绘制。 - 叠放顺序:后添加的子组件,默认绘制在先添加的子组件的"上方"。
2.2 Stack 的典型使用场景
| 场景 | 说明 |
|---|---|
| 悬浮按钮 | FAB 浮在页面内容之上 |
| 遮罩 / 弹窗 | 半透明遮罩覆盖主内容 |
| 卡片嵌套 | 多张卡片交错堆叠的效果 |
| 游戏 UI | 血条、计分板浮在游戏画面之上 |
| 图片标注 | 文字标签浮在图片的特定位置 |
| 引导蒙层 | 高亮指示浮在界面之上 |
2.3 与 CSS 中 position: absolute 的类比
如果您有 Web 开发背景,可以将 Stack 理解为 position: relative 容器,而它的直接子组件相当于 position: absolute,通过 left / top(即 position({x, y}))进行自由定位,并通过 CSS z-index 控制层叠顺序。
三、zIndex(ZOrder) 深入理解
3.1 什么是 zIndex?
在 ArkUI 中,屏幕的坐标系是一个三维空间:
- X 轴:水平方向(向右为正)
- Y 轴:垂直方向(向下为正)
- Z 轴:垂直于屏幕方向(指向用户为正)
zIndex 属性控制的就是组件在 Z 轴 上的位置——值越大,组件越靠近用户(即视觉上越靠前)。
3.2 zIndex 的核心规则
// 语法:.zIndex(value: number)
Component()
.zIndex(0) // 默认值
规则一:值越大越靠前
zIndex = 10的组件一定在zIndex = 0的组件之上。- 即使
zIndex = 3的组件在代码中先声明,它依然会覆盖zIndex = 1的后声明组件。
规则二:默认值为 0
所有组件未指定 zIndex 时,默认值为 0。这意味着您只需对需要"浮出"的组件设置 zIndex > 0 即可。
规则三:可为负数
zIndex 可以是负数(如 -1、-5)。设置为负数的组件会退到所有默认层级组件之下,适用于"背景装饰"类元素。
规则四:相同 zIndex 时,后添加的覆盖先添加的
如果两个或多个组件具有相同的 zIndex 值,则它们在代码中的声明顺序决定叠放顺序——写在后面的组件覆盖写在前面的组件。
3.3 v.s. CSS z-index —— 异同对比
| 对比维度 | CSS z-index | ArkUI .zIndex() |
|---|---|---|
| 数值范围 | 整数,默认 auto | 整数,默认 0 |
| 负数 | 支持 | 支持 |
| 定位上下文 | 需要 position: relative/absolute 等 |
任何组件都可用(但 Stack 中最有效) |
| 默认值 | auto(按 DOM 流顺序) |
0 |
| 父级影响 | 受父级 stacking context 限制 | 在 Stack 内线性生效 |
四、Demo 应用全解析
本文附带的 Demo 应用 StackZOrderDemo.ets 是一个交互式演示程序,让您通过点击按钮实时感受 zIndex 的变化效果。
4.1 应用整体结构
StackZOrderDemo (主页 @Entry @Component)
├── Column (根容器)
│ ├── Text (标题)
│ ├── Stack (核心演示区域)
│ │ ├── Column → 方块1 (红色, zIndex=2)
│ │ ├── Column → 方块2 (绿色, zIndex=1)
│ │ └── Column → 方块3 (蓝色, zIndex=0)
│ ├── Text (叠放顺序提示)
│ ├── Row × 3 (控制面板: 每个方块的 +/-/0 按钮)
│ ├── Row (快捷操作: 归零/恢复/反转)
│ └── Column (规则说明)
└── getOrderHint() (辅助方法)
4.2 核心代码逐段解析
4.2.1 页面入口与状态定义
@Entry
@Component
struct StackZOrderDemo {
@State zIndex1: number = 2
@State zIndex2: number = 1
@State zIndex3: number = 0
这里定义了三个 @State 状态变量,分别对应三个方块的 zIndex 值。@State 装饰器确保当值变化时,ArkUI 会自动重新渲染相关组件。
初始叠放顺序:方块1(z=2) > 方块2(z=1) > 方块3(z=0)
4.2.2 Stack 核心层叠区域
Stack() {
// 方块1(红色)
Column() {
Rect().width(120).height(120).fill('#FF6B6B')
.radiusWidth(12).radiusHeight(12)
Text('方块1\nz=' + this.zIndex1)
.fontSize(12).fontColor(Color.White)
.textAlign(TextAlign.Center)
}
.width(120).height(150)
.position({ x: 20, y: 20 })
.zIndex(this.zIndex1) // ⭐ 核心:zIndex 控制层级
// 方块2(绿色)
Column() {
Rect().width(120).height(120).fill('#51CF66').radiusWidth(12).radiusHeight(12)
Text('方块2\nz=' + this.zIndex2).fontSize(12).fontColor(Color.White).textAlign(TextAlign.Center)
}
.width(120).height(150)
.position({ x: 80, y: 80 })
.zIndex(this.zIndex2)
// 方块3(蓝色)
Column() {
Rect().width(120).height(120).fill('#5C7CFA').radiusWidth(12).radiusHeight(12)
Text('方块3\nz=' + this.zIndex3).fontSize(12).fontColor(Color.White).textAlign(TextAlign.Center)
}
.width(120).height(150)
.position({ x: 140, y: 140 })
.zIndex(this.zIndex3)
}
.width(300).height(300)
.backgroundColor('#EDF2FF')
.borderRadius(16)
.border({ width: 2, color: '#DEE2E6' })
这段代码的要点:
Stack()作为容器 — 没有传入参数,使用默认行为。- 子组件通过
.position({ x, y })偏移 — 三个方块的偏移量依次递增 (20,20)、(80,80)、(140,140),实现错位露出效果,让叠放关系一目了然。 - 每个方块是一个 Column — Column 内包含一个圆角矩形
Rect和一个文字标签Text,Column 本身作为 Stack 的子组件接受.zIndex()控制。 .zIndex()绑定 @State 变量 — 当点击按钮修改 zIndex 值时,Stack 自动重新计算层叠顺序并重绘。
4.2.3 交互控制面板
// 每个方块独立控制
Row() {
Circle().width(14).height(14).fill('#FF6B6B') // 颜色标识
Text('方块1 zIndex = ' + this.zIndex1).fontSize(14).width(140)
Button('-').width(36).height(32).onClick(() => { this.zIndex1-- })
Button('+').width(36).height(32).onClick(() => { this.zIndex1++ })
Button('0').width(36).height(32).onClick(() => { this.zIndex1 = 0 })
}
每个方块都有一个控制行,包含:
Circle()颜色圆点:直观标识对应的方块颜色Text显示当前值:实时展示 zIndex 数值-按钮:zIndex 减 1+按钮:zIndex 加 10按钮:重置 zIndex 为 0
4.2.4 辅助方法:动态排序提示
getOrderHint(): string {
let arr: number[] = [this.zIndex1, this.zIndex2, this.zIndex3]
let idx: number[] = [1, 2, 3]
for (let i = 0; i < 3; i++) {
for (let j = i + 1; j < 3; j++) {
if (arr[i] < arr[j]) {
let t = arr[i]; arr[i] = arr[j]; arr[j] = t
let ti = idx[i]; idx[i] = idx[j]; idx[j] = ti
}
}
}
return '叠放顺序(上层→下层):方块' + idx[0] + ' > 方块' + idx[1] + ' > 方块' + idx[2]
}
这是一个纯逻辑辅助函数,使用冒泡排序按 zIndex 降序排列三个方块,生成如 “叠放顺序(上层→下层):方块1 > 方块3 > 方块2” 的文字提示,帮助用户理解当前的层级关系。
4.2.5 快捷操作
Row() {
Button('全部归零').height(36).onClick(() => {
this.zIndex1 = 0; this.zIndex2 = 0; this.zIndex3 = 0
})
Button('恢复初始').height(36).onClick(() => {
this.zIndex1 = 2; this.zIndex2 = 1; this.zIndex3 = 0
})
Button('反转顺序').height(36).onClick(() => {
let t = this.zIndex1
this.zIndex1 = this.zIndex3
this.zIndex3 = t
})
}.width('100%').justifyContent(FlexAlign.SpaceEvenly)
三个快捷按钮方便快速演示:
- 全部归零:所有方块
zIndex = 0,回到"相同 zIndex,后添加覆盖先添加"的默认行为 - 恢复初始:回到预设的 2 > 1 > 0 层级顺序
- 反转顺序:交换方块1和方块3的 zIndex,直观感受叠放反转的效果
4.3 运行效果预览
运行后,您将看到:
- 一个浅蓝色背景的 300×300vp Stack 区域,内部有三个带圆角的彩色方块(红、绿、蓝)错位排列。
- 初始状态下:红色(z=2)完全覆盖绿色和蓝色,绿色(z=1)覆盖蓝色(z=0),蓝色在最底层。
- 点击方块1的
-按钮将其 zIndex 减到 0,红色方块会"沉"到底部。 - 点击方块3的
+按钮将其 zIndex 增加到 3,蓝色方块会"浮"到最上层。 - 点击"反转顺序",红蓝方块交换层级。
- 顶部的文字提示会同步更新当前三者的叠放顺序。
五、Stack + zIndex 常见陷阱与最佳实践
5.1 陷阱一:忘记 .position() 导致完全重叠
Stack 的默认行为是所有子组件从 (0,0) 开始层叠。如果您不通过 .position() 设置偏移,所有方块将完全重叠,只能看到最上层的组件,无法观察叠放关系。
✅ 最佳实践:调试阶段先给子组件设置不同的 .position() 偏移,确认层级后再调整精准位置。
5.2 陷阱二:超出 Stack 边界被裁剪
默认情况下,Stack 会对超出其宽高范围的子组件进行裁剪。如果您需要子组件显示在 Stack 边界之外(如弹出菜单、工具提示),请设置:
Stack()
.clip(false) // 关闭裁剪
5.3 陷阱三:zIndex 与 opacity 的交互
当子组件设置了透明度(opacity < 1)时,下方组件会透过上方组件显示出来。这是一个非常有用的视觉技巧,但要注意:
- 透明度值越小,下方组件越清晰可见
- 如果不需要透视效果,保持
opacity = 1(默认值)
5.4 陷阱四:在非 Stack 容器中使用 zIndex
.zIndex() 并非 Stack 专属 —— 它可以在任何组件上使用。但在 Column、Row、Flex 等线性布局容器中,子组件按其布局方向排列,zIndex 仅影响视觉层叠而不影响布局位置。
✅ 最佳实践:需要自由定位 + 层叠控制时使用 Stack;仅仅需要调整层级时可以在任何容器中单独使用 .zIndex()。
5.5 陷阱五:页面注册(新手常见白屏问题)
在 HarmonyOS NEXT 中,所有页面必须先在 main_pages.json 中注册:
{
"src": [
"pages/Index",
"pages/StackZOrderDemo"
]
}
否则即使 EntryAbility 中调用 loadContent('pages/StackZOrderDemo') 也会白屏。
5.6 陷阱六:全局内置组件无需 import
Stack、Column、Row、Text、Button、Rect、Circle、Scroll 等是 ArkUI 全局内置组件,不要从 @kit.ArkUI 中导入它们。错误的导入会导致渲染白屏:
// ❌ 错误:Stack 是全局内置组件,无需导入
import { Stack } from '@kit.ArkUI'
// ✅ 正确:直接使用
Stack() { ... }
六、进阶应用:多场景实战
6.1 场景一:图片 + 文字标注浮层
Stack() {
Image($r('app.media.photo'))
.width('100%').height(300)
.objectFit(ImageFit.Cover)
// 半透明遮罩
Column()
.width('100%').height(60)
.position({ x: 0, y: 240 })
.backgroundColor('#88000000')
// 文字标注(浮在遮罩之上)
Text('风景如画 · 拍摄于黄山')
.fontSize(16).fontColor(Color.White)
.position({ x: 20, y: 250 })
.zIndex(1)
}
6.2 场景二:悬浮操作按钮(FAB)
Stack() {
// 主内容
List() { /* 列表内容 */ }
.width('100%').height('100%')
// FAB(悬浮在右下角)
Button() {
Image($r('app.media.ic_add'))
.width(24).height(24)
}
.width(56).height(56)
.backgroundColor('#007AFF')
.borderRadius(28)
.position({ x: '80%', y: '85%' })
.zIndex(10) // 确保浮在所有内容之上
.shadow({ radius: 8, color: '#33000000' })
}
6.3 场景三:卡片层叠效果(类似 Apple Wallet)
Stack() {
ForEach(cards, (card: Card, index: number) => {
CardItem({ data: card })
.position({ x: 0, y: index * 20 })
.zIndex(cards.length - index) // 第一张卡片 zIndex 最大
})
}
这种模式利用 zIndex 让每张卡片"浮"在下一张之上,配合位置偏移实现层叠卡片效果。
6.4 场景四:动态排序拖拽层叠
结合手势系统(.gesture() + PanGesture),可以实现拖拽方块改变层叠顺序的效果——拖拽中的方块设置 zIndex = 100 临时浮在所有方块之上,释放后根据位置重新分配 zIndex。
七、性能考量
zIndex 本身不会带来显著性能开销,它是一个纯布局属性,影响的是渲染阶段的合成顺序,而不是测量和布局阶段。
以下情况需注意性能:
- 频繁动态修改 zIndex:每次修改都会触发
@State驱动的刷新,合理合并多次修改可减少不必要的重绘。 - 大量子组件:如果 Stack 中有几十上百个子组件,且频繁变动 zIndex,建议使用
LazyForEach做虚拟化渲染。 - 嵌套 Stack:避免多层 Stack 嵌套,尽量保持扁平化层级结构。
八、总结
通过本文的完整 Demo 和深入分析,我们掌握了以下核心知识点:
- Stack 容器:一种允许子组件在 Z 轴上层叠的自由布局容器,子组件默认从左上角开始堆叠。
- zIndex 属性:控制组件在 Z 轴上的视觉层级,值越大越靠近用户,默认值为 0,支持负数。
- 四条叠放规则:值大者覆盖值小者;默认值为 0;相同值时后添加覆盖先添加;负数可退到底层。
- 交互式验证:通过状态变量
@State+ 按钮事件,实时调整 zIndex,直观验证叠放规则。 - 工程注意事项:页面需注册到
main_pages.json、全局组件无需 import、注意.clip()对裁剪的影响。
Stack + zIndex 是构建复杂 UI 不可或缺的基础组合。无论是简单的悬浮按钮,还是复杂的多层游戏界面,理解并熟练运用这套机制,将让您的鸿蒙原生应用开发事半功倍。
九、参考资料
- HarmonyOS NEXT 开发者文档 — Stack 容器
- HarmonyOS NEXT 开发者文档 — zIndex 属性
- ArkTS 语言规范
- HarmonyOS NEXT API 24 Release Notes
版权声明:本文为原创技术博客,遵循 CC BY-NC-SA 4.0 协议。如需转载,请注明出处。
配套源码:本博客配套的完整 Demo 代码StackZOrderDemo.ets已附于本文同目录下。
更多推荐



所有评论(0)