鸿蒙原生 ArkTS 布局深度解析:Stack 容器与 ZOrder 层级控制实战


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

HarmonyOS NEXT 作为华为自研的全场景分布式操作系统,从底层彻底剥离了 AOSP 代码,实现了"纯鸿蒙"的生态闭环。随之而来的 ArkTS(Ark TypeScript)语言也演进到了 3.0 版本,成为构建鸿蒙原生应用的核心语言。

在 ArkUI 声明式 UI 框架中,布局系统是最基础也最重要的能力之一。与前端领域的 CSS Flexbox、Grid 类似,ArkUI 提供了一系列布局容器组件来帮助开发者组织界面元素。其中,Stack 容器 是一种"自由层叠式"布局容器,允许子组件在 Z 轴方向上叠放组合,是实现复杂 UI 效果(悬浮按钮、模态弹窗、卡片叠加、游戏 UI)的利器。

然而,许多开发者在使用 Stack 时,常常困惑于两个问题:

  1. 各个子组件之间的叠放顺序是如何确定的?
  2. 如何精确控制某个子组件浮在另一组件的上方或下方?

答案是: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' })

这段代码的要点:

  1. Stack() 作为容器 — 没有传入参数,使用默认行为。
  2. 子组件通过 .position({ x, y }) 偏移 — 三个方块的偏移量依次递增 (20,20)、(80,80)、(140,140),实现错位露出效果,让叠放关系一目了然。
  3. 每个方块是一个 Column — Column 内包含一个圆角矩形 Rect 和一个文字标签 Text,Column 本身作为 Stack 的子组件接受 .zIndex() 控制。
  4. .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 加 1
  • 0 按钮:重置 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 运行效果预览

运行后,您将看到:

  1. 一个浅蓝色背景的 300×300vp Stack 区域,内部有三个带圆角的彩色方块(红、绿、蓝)错位排列。
  2. 初始状态下:红色(z=2)完全覆盖绿色和蓝色,绿色(z=1)覆盖蓝色(z=0),蓝色在最底层。
  3. 点击方块1的 - 按钮将其 zIndex 减到 0,红色方块会"沉"到底部。
  4. 点击方块3的 + 按钮将其 zIndex 增加到 3,蓝色方块会"浮"到最上层。
  5. 点击"反转顺序",红蓝方块交换层级。
  6. 顶部的文字提示会同步更新当前三者的叠放顺序。

五、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 专属 —— 它可以在任何组件上使用。但在 ColumnRowFlex 等线性布局容器中,子组件按其布局方向排列,zIndex 仅影响视觉层叠而不影响布局位置

✅ 最佳实践:需要自由定位 + 层叠控制时使用 Stack;仅仅需要调整层级时可以在任何容器中单独使用 .zIndex()

5.5 陷阱五:页面注册(新手常见白屏问题)

在 HarmonyOS NEXT 中,所有页面必须先在 main_pages.json 中注册:

{
  "src": [
    "pages/Index",
    "pages/StackZOrderDemo"
  ]
}

否则即使 EntryAbility 中调用 loadContent('pages/StackZOrderDemo') 也会白屏。

5.6 陷阱六:全局内置组件无需 import

StackColumnRowTextButtonRectCircleScroll 等是 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 本身不会带来显著性能开销,它是一个纯布局属性,影响的是渲染阶段的合成顺序,而不是测量和布局阶段。

以下情况需注意性能:

  1. 频繁动态修改 zIndex:每次修改都会触发 @State 驱动的刷新,合理合并多次修改可减少不必要的重绘。
  2. 大量子组件:如果 Stack 中有几十上百个子组件,且频繁变动 zIndex,建议使用 LazyForEach 做虚拟化渲染。
  3. 嵌套 Stack:避免多层 Stack 嵌套,尽量保持扁平化层级结构。

八、总结

通过本文的完整 Demo 和深入分析,我们掌握了以下核心知识点:

  1. Stack 容器:一种允许子组件在 Z 轴上层叠的自由布局容器,子组件默认从左上角开始堆叠。
  2. zIndex 属性:控制组件在 Z 轴上的视觉层级,值越大越靠近用户,默认值为 0,支持负数。
  3. 四条叠放规则:值大者覆盖值小者;默认值为 0;相同值时后添加覆盖先添加;负数可退到底层。
  4. 交互式验证:通过状态变量 @State + 按钮事件,实时调整 zIndex,直观验证叠放规则。
  5. 工程注意事项:页面需注册到 main_pages.json、全局组件无需 import、注意 .clip() 对裁剪的影响。

Stack + zIndex 是构建复杂 UI 不可或缺的基础组合。无论是简单的悬浮按钮,还是复杂的多层游戏界面,理解并熟练运用这套机制,将让您的鸿蒙原生应用开发事半功倍。


九、参考资料


版权声明:本文为原创技术博客,遵循 CC BY-NC-SA 4.0 协议。如需转载,请注明出处。
配套源码:本博客配套的完整 Demo 代码 StackZOrderDemo.ets 已附于本文同目录下。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐