HarmonyOS 6.1 实战:Stack 层叠布局与 Overlay 浮层详解

前言

在移动应用开发中,层叠布局是实现复杂 UI 的核心能力------从头像角标到全屏蒙层,从浮动按钮到图片水印,都离不开精确的层叠控制。HarmonyOS ArkUI 提供了 Stack 组件与 .overlay() 修饰符来应对这类场景。本文通过完整可运行的代码,逐一演示它们的核心用法。

运行效果

初始状态

交互后(切换对齐 + z-index)

核心 API 概览

API 作用
Stack({ alignContent }) 子组件全部叠放,alignContent 控制默认对齐点
.zIndex(n) 调整单个子组件的层叠顺序,数值越大越靠前
.overlay(builder, options) 在组件上方浮置内容,不占布局空间
.offset({ x, y }) 相对自身默认位置平移,常与 Stack 配合使用

完整示例代码

typescript 复制代码
// Stack 层叠布局与 Overlay 浮层 Demo

class AlignOption {
  label: string = ''
  value: Alignment = Alignment.Center
}

@Entry
@Component
struct Index {
  @State alignIndex: number = 4  // default Center
  @State zToggle: boolean = false
  @State showOverlay: boolean = true

  private alignOptions: AlignOption[] = [
    { label: 'TopStart', value: Alignment.TopStart },
    { label: 'Top', value: Alignment.Top },
    { label: 'TopEnd', value: Alignment.TopEnd },
    { label: 'Start', value: Alignment.Start },
    { label: 'Center', value: Alignment.Center },
    { label: 'End', value: Alignment.End },
    { label: 'BottomStart', value: Alignment.BottomStart },
    { label: 'Bottom', value: Alignment.Bottom },
    { label: 'BottomEnd', value: Alignment.BottomEnd },
  ]

  // overlay 内容需要用 @Builder 封装,不能内联组件表达式
  @Builder
  overlayContent() {
    if (this.showOverlay) {
      Column() {
        Text('🎨 overlay 叠加层')
          .fontSize(18)
          .fontColor('#fff')
          .fontWeight(FontWeight.Bold)
        Text('半透明背景效果')
          .fontSize(12)
          .fontColor('rgba(255,255,255,0.8)')
          .margin({ top: 4 })
      }
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(0,0,0,0.45)')
      .justifyContent(FlexAlign.Center)
      .borderRadius(12)
    }
  }

  @Builder
  overlayBadgeHot() {
    Text('HOT 🔥')
      .fontSize(11)
      .fontColor('#fff')
      .backgroundColor('#ff3300')
      .borderRadius(4)
      .padding({ left: 6, right: 6, top: 2, bottom: 2 })
  }

  @Builder
  overlayBadgeNew() {
    Text('NEW ✨')
      .fontSize(11)
      .fontColor('#fff')
      .backgroundColor('#0066ff')
      .borderRadius(4)
      .padding({ left: 6, right: 6, top: 2, bottom: 2 })
  }

  build() {
    Stack({ alignContent: Alignment.BottomEnd }) {
      Scroll() {
        Column({ space: 0 }) {
          Text('Stack 层叠布局 & Overlay')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1a1a1a')
            .width('100%')
            .padding({ left: 16, right: 16, top: 20, bottom: 8 })

          // ① alignContent 九宫格选择器
          Text('① alignContent 九宫格对齐')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333')
            .width('100%')
            .padding({ left: 16, top: 12, bottom: 6 })

          Column({ space: 4 }) {
            Text('当前: ' + this.alignOptions[this.alignIndex].label)
              .fontSize(13).fontColor('#0066ff').margin({ bottom: 8 })

            Row({ space: 6 }) {
              Button('TL').fontSize(11).width(72).height(32)
                .backgroundColor(this.alignIndex === 0 ? '#0066ff' : '#e8e8e8')
                .fontColor(this.alignIndex === 0 ? '#fff' : '#333')
                .onClick(() => { this.alignIndex = 0 })
              Button('Top').fontSize(11).width(72).height(32)
                .backgroundColor(this.alignIndex === 1 ? '#0066ff' : '#e8e8e8')
                .fontColor(this.alignIndex === 1 ? '#fff' : '#333')
                .onClick(() => { this.alignIndex = 1 })
              Button('TR').fontSize(11).width(72).height(32)
                .backgroundColor(this.alignIndex === 2 ? '#0066ff' : '#e8e8e8')
                .fontColor(this.alignIndex === 2 ? '#fff' : '#333')
                .onClick(() => { this.alignIndex = 2 })
            }
            Row({ space: 6 }) {
              Button('Start').fontSize(11).width(72).height(32)
                .backgroundColor(this.alignIndex === 3 ? '#0066ff' : '#e8e8e8')
                .fontColor(this.alignIndex === 3 ? '#fff' : '#333')
                .onClick(() => { this.alignIndex = 3 })
              Button('Center').fontSize(11).width(72).height(32)
                .backgroundColor(this.alignIndex === 4 ? '#0066ff' : '#e8e8e8')
                .fontColor(this.alignIndex === 4 ? '#fff' : '#333')
                .onClick(() => { this.alignIndex = 4 })
              Button('End').fontSize(11).width(72).height(32)
                .backgroundColor(this.alignIndex === 5 ? '#0066ff' : '#e8e8e8')
                .fontColor(this.alignIndex === 5 ? '#fff' : '#333')
                .onClick(() => { this.alignIndex = 5 })
            }
            Row({ space: 6 }) {
              Button('BL').fontSize(11).width(72).height(32)
                .backgroundColor(this.alignIndex === 6 ? '#0066ff' : '#e8e8e8')
                .fontColor(this.alignIndex === 6 ? '#fff' : '#333')
                .onClick(() => { this.alignIndex = 6 })
              Button('Bottom').fontSize(11).width(72).height(32)
                .backgroundColor(this.alignIndex === 7 ? '#0066ff' : '#e8e8e8')
                .fontColor(this.alignIndex === 7 ? '#fff' : '#333')
                .onClick(() => { this.alignIndex = 7 })
              Button('BR').fontSize(11).width(72).height(32)
                .backgroundColor(this.alignIndex === 8 ? '#0066ff' : '#e8e8e8')
                .fontColor(this.alignIndex === 8 ? '#fff' : '#333')
                .onClick(() => { this.alignIndex = 8 })
            }
          }
          .width('100%')
          .alignItems(HorizontalAlign.Center)
          .padding({ left: 16, right: 16 })

          // Stack 预览
          Stack({ alignContent: this.alignOptions[this.alignIndex].value }) {
            Column()
              .width('100%').height(180)
              .backgroundColor('#e8f0ff').borderRadius(12)
            Column() {
              Text(this.alignOptions[this.alignIndex].label)
                .fontSize(14).fontColor('#fff').fontWeight(FontWeight.Bold)
            }
            .width(100).height(44)
            .backgroundColor('#0066ff').borderRadius(8)
            .justifyContent(FlexAlign.Center)
          }
          .width('100%').height(180)
          .margin({ left: 16, right: 16, top: 12 })
          .borderRadius(12).clip(true)

          // ② z-index 层级
          Text('② z-index 层级控制')
            .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#333')
            .width('100%').padding({ left: 16, top: 20, bottom: 6 })

          Button(this.zToggle ? '恢复默认层级' : '切换 z-index')
            .fontSize(13).height(36).backgroundColor('#ff6600')
            .margin({ left: 16, bottom: 10 })
            .onClick(() => { this.zToggle = !this.zToggle })

          Stack() {
            Column() {
              Text('Box 1').fontSize(12).fontColor('#fff')
              Text('z=' + (this.zToggle ? '3' : '1')).fontSize(11).fontColor('rgba(255,255,255,0.8)')
            }
            .width(140).height(100).backgroundColor('#ff4d4d')
            .borderRadius(8).justifyContent(FlexAlign.Center)
            .zIndex(this.zToggle ? 3 : 1).offset({ x: 0, y: 0 })

            Column() {
              Text('Box 2').fontSize(12).fontColor('#fff')
              Text('z=' + (this.zToggle ? '1' : '2')).fontSize(11).fontColor('rgba(255,255,255,0.8)')
            }
            .width(140).height(100).backgroundColor('#33aa66')
            .borderRadius(8).justifyContent(FlexAlign.Center)
            .zIndex(this.zToggle ? 1 : 2).offset({ x: 40, y: 20 })

            Column() {
              Text('Box 3').fontSize(12).fontColor('#fff')
              Text('z=' + (this.zToggle ? '2' : '3')).fontSize(11).fontColor('rgba(255,255,255,0.8)')
            }
            .width(140).height(100).backgroundColor('#0066ff')
            .borderRadius(8).justifyContent(FlexAlign.Center)
            .zIndex(this.zToggle ? 2 : 3).offset({ x: 80, y: 40 })
          }
          .width('100%').height(160).padding({ left: 16 })

          // ③ overlay 叠加层
          Text('③ overlay 叠加层')
            .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#333')
            .width('100%').padding({ left: 16, top: 20, bottom: 6 })

          Button(this.showOverlay ? '隐藏 overlay' : '显示 overlay')
            .fontSize(13).height(36).backgroundColor('#9933cc')
            .margin({ left: 16, bottom: 10 })
            .onClick(() => { this.showOverlay = !this.showOverlay })

          Column()
            .width('100%').height(150)
            .backgroundColor('#3366cc').borderRadius(12)
            .margin({ left: 16, right: 16 })
            // overlay 必须传 @Builder 引用,不能传内联组件
            .overlay(this.overlayContent, { align: Alignment.Center })

          // ④ 图片卡片角标
          Text('④ 图片卡片 overlay 角标')
            .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#333')
            .width('100%').padding({ left: 16, top: 20, bottom: 6 })

          Row({ space: 10 }) {
            Column()
              .width(150).height(100)
              .backgroundColor('#ff9900').borderRadius(10)
              .overlay(this.overlayBadgeHot, { align: Alignment.TopEnd })

            Column()
              .width(150).height(100)
              .backgroundColor('#00aa55').borderRadius(10)
              .overlay(this.overlayBadgeNew, { align: Alignment.TopStart })
          }
          .padding({ left: 16 })

          Column().height(100)
        }
        .width('100%')
      }
      .scrollable(ScrollDirection.Vertical)
      .scrollBar(BarState.Auto)
      .width('100%').height('100%')

      // 悬浮 FAB(利用 Stack alignContent.BottomEnd)
      Stack({ alignContent: Alignment.Center }) {
        Column()
          .width(56).height(56)
          .backgroundColor('#0066ff').borderRadius(28)
          .shadow({ radius: 8, color: 'rgba(0,102,255,0.4)', offsetX: 0, offsetY: 4 })
        Text('+')
          .fontSize(28).fontColor('#fff').fontWeight(FontWeight.Bold)
          .margin({ bottom: 2 })
      }
      .width(56).height(56)
      .margin({ right: 20, bottom: 20 })
      .onClick(() => {
        this.alignIndex = (this.alignIndex + 1) % 9
      })
    }
    .width('100%').height('100%')
    .backgroundColor('#f8f9fa')
  }
}

关键知识点

1. overlay() 必须传 @Builder 引用

.overlay() 的类型签名是 string | CustomBuilder | ComponentContent<Object>不能 将内联组件表达式(如 Column() { ... } 结果、Text(...) 结果)直接传入,必须封装为 @Builder 方法再传引用:

typescript 复制代码
// 错误:直接传内联 Column 表达式
.overlay(Column() { Text('hello') }, { align: Alignment.Center })

// 正确:用 @Builder 封装后传引用
@Builder myOverlay() {
  Column() { Text('hello') }
}

.overlay(this.myOverlay, { align: Alignment.Center })

2. @Builder 内可以有 if 判断

@Builder 内允许 if/else 控制流,但不允许 let/const 变量声明 。通过 @State 响应式变量 + if 来控制浮层的显隐:

typescript 复制代码
@Builder
overlayContent() {
  if (this.showOverlay) {
    Column() { ... }
      .backgroundColor('rgba(0,0,0,0.45)')
  }
}

3. overlay 不占布局空间

overlay 内容浮于组件之上,不影响父容器的布局计算,这与在 Stack 中额外增加子组件不同:

方式 占布局空间 适合场景
Stack 子组件 是(占用 Stack 尺寸) 多层交叠时需要 z-index 精确控制
overlay 角标、蒙层、水印,不影响周围布局

4. zIndex 改变渲染顺序

默认情况下 Stack 子组件按声明顺序叠放(后声明的在上层)。.zIndex(n) 可打破这一规则:

typescript 复制代码
Stack() {
  Column().zIndex(3)  // 虽然先声明,但层级最高,显示在最上面
  Column().zIndex(1)
  Column().zIndex(2)
}

常见应用场景

场景 实现方式
头像右下角角标 overlay(badgeBuilder, { align: Alignment.BottomEnd })
全屏蒙层 overlay(maskBuilder) + 宽高 100%,backgroundColor 半透明
悬浮操作按钮 Stack + 按钮子组件 + alignContent: Alignment.BottomEnd
图片水印 overlay(watermarkBuilder, { align: Alignment.BottomStart })
卡片角标(HOT/NEW) overlay(badgeBuilder, { align: Alignment.TopEnd })

小结

  • Stack 让所有子组件叠放在同一区域,alignContent 控制默认对齐点,.zIndex() 控制层叠顺序
  • .overlay() 是轻量浮层方案,不占布局空间,内容必须以 @Builder 引用传入
  • @Builder 方法内允许 if/else,但不允许 let/const 变量声明
  • 悬浮 FAB 直接利用最外层 StackalignContent.BottomEnd,无需额外定位
相关推荐
程序员cxuan2 小时前
离谱,GPT-5.6 竟然在后台执行了 rm -rf
人工智能·后端·程序员
Csvn2 小时前
📊 SQL 入门 Day 9:CTE(公用表表达式)— 让 SQL 像写代码一样优雅
后端·sql
运维行者_2 小时前
如何查看每个IP的带宽使用情况?NetFlow 技术实战指南
开发语言·网络·分布式·后端·架构·带宽
济*沧*海3 小时前
如何判断接口的返回值?
后端
杨充3 小时前
5.多用组合和少继承
后端
杨充3 小时前
4.接口而非实现编程
java·后端·架构
一缕清烟在人间3 小时前
HarmonyOS应用开发实战:萌宠日记 - 生命周期在萌宠日记中的实践
后端
Conan在掘金3 小时前
鸿蒙 ArkUI 组件深水区:Span 富文本嵌套,一行 Text 塞下「红蓝斜下大」五样式 + 动态高亮
后端
卷无止境3 小时前
Python 类型注解与运行时反射:从原理到工程实践
后端·python