鸿蒙原生 ArkTS 布局方式之 Stack 实现遮罩层 / 半透明蒙版


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

一、引言

在移动端应用中,模态弹窗是最常见的交互模式之一。一个标准的模态弹窗由两部分构成:

  1. 半透明遮罩层 — 覆盖在主内容之上,视觉上"变暗"背景,引导用户聚焦弹窗;
  2. 弹窗卡片 — 位于遮罩之上,承载操作内容。

在鸿蒙 ArkTS 布局体系中,实现"层叠 + 半透明"效果的最佳方案就是使用 Stack 布局 配合 .opacity() 透明度控制。


二、Stack 布局基础

2.1 什么是 Stack

Stack(层叠布局)是鸿蒙 ArkTS 中最重要的布局容器之一。与 Column(纵向排列)和 Row(横向排列)不同,Stack 将其子组件沿 Z 轴(垂直屏幕方向) 依次叠放:

  • 第一个子组件位于最底层(Z 序最小);
  • 后续子组件依次叠加在上层(Z 序递增);
  • 后写入的组件在视觉上覆盖先写入的组件。

这种"层叠"特性,使得 Stack 成为实现遮罩、弹窗、悬浮按钮等场景的不二之选。

2.2 Stack 的核心属性

Stack() {
  // 子组件按 Z 序从下到上写入
}
.width('100%').height('100%')
.alignContent(Alignment.Center)   // 子组件对齐方式
.clip(false)                       // 不裁剪,确保阴影完整

常用对齐方式:

Alignment 效果
Alignment.TopStart 左上角
Alignment.Center 正中央(最常用)
Alignment.BottomEnd 右下角

三、遮罩层的核心原理

3.1 遮罩的本质

遮罩层本质上是一个铺满全屏的矩形,核心特性:全尺寸(100% × 100%)+ 黑色 + 半透明(opacity 0.5)+ 点击可关闭

3.2 为什么用 opacity 而不是 ARGB

有人问:既然黑色半透明可以用 #80000000(ARGB)表达,为何还用 .opacity(0.5)

原因有二:

  1. 语义清晰.opacity(0.5) 明确表达"50% 透明度"的意图;
  2. 动画自然.transition() 直接操作 .opacity 属性做动画,ARGB 的固定 alpha 值无法平滑过渡。

推荐做法:fill(Color.Black) + .opacity(0.5) 组合


四、完整示例代码

4.1 三层 Stack 架构

Stack(根容器, alignContent: Center)
├── 层次 1:主页面内容(Column + Scroll + 按钮)
├── [if] 层次 2:Rectangle().fill(Black).opacity(0.5)  ← 半透明遮罩
└── [if] 层次 3:模态弹窗卡片(居中显示)

4.2 完整代码

/*
 * Stack + .opacity() + 条件渲染 实现半透明遮罩
 */
import { Curves, TransitionEffect } from '@kit.ArkUI';

@Entry
@Component
struct StackMaskDemo {
  @State isMaskVisible: boolean = false;

  build() {
    Stack() {
      // ===== 层次 1:主页面内容 =====
      Column() {
        Row() {
          Text('Stack 遮罩层示例')
            .fontSize(22).fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
        }
        .width('100%').height(56)
        .backgroundColor('#3F6AE8')
        .justifyContent(FlexAlign.Center)

        Scroll() {
          Column() {
            CardContent({
              title: '📦 Stack 布局',
              description: 'Stack(层叠布局)将子组件按 Z 轴叠放,'
                + '后写入的组件在上层。适合遮罩、弹窗等场景。'
            })
            CardContent({
              title: '🔑 遮罩层核心',
              description: '1. 外层 Stack 包裹全部内容\n'
                + '2. 第 1 层:主页面\n'
                + '3. 第 2 层:Rectangle().opacity(0.5)\n'
                + '4. 第 3 层:模态弹窗\n'
                + '5. .transition() 实现动画'
            })
            CardContent({
              title: '👆 操作说明',
              description: '点击按钮弹出模态弹窗。\n'
                + '点击遮罩区域可关闭弹窗。\n'
                + '观察入场/出场动画效果。'
            })
          }.width('100%').padding(16)
        }
        .layoutWeight(1).backgroundColor('#F5F5F5')

        Row() {
          Button('📌 显示模态弹窗')
            .width(200).height(48)
            .backgroundColor('#3F6AE8').borderRadius(24)
            .fontColor(Color.White).fontSize(16)
            .onClick(() => { this.isMaskVisible = true; })
        }
        .width('100%').height(80)
        .justifyContent(FlexAlign.Center)
        .backgroundColor(Color.White)
      }.width('100%').height('100%')

      // ===== 层次 2:半透明遮罩 =====
      if (this.isMaskVisible) {
        Rectangle()
          .width('100%').height('100%')
          .fill(Color.Black)
          .opacity(0.5)                    // ← 半透明蒙版
          .onClick(() => { this.isMaskVisible = false; })
          .transition(
            TransitionEffect.opacity(0)
              .animation({ duration: 300, curve: Curves.FastOutSlowIn })
          )
      }

      // ===== 层次 3:模态弹窗 =====
      if (this.isMaskVisible) {
        Column() {
          Text('📋 模态弹窗').fontSize(20)
            .fontWeight(FontWeight.Bold).fontColor('#333333')
          Divider().width('90%').color('#E0E0E0').margin({ top: 8, bottom: 16 })
          Text('这是使用 Stack + .opacity() 实现的模态弹窗。')
            .fontSize(15).fontColor('#666666')
            .width('90%').textAlign(TextAlign.Start)
          Row() {
            Button('取消').width(100).height(40)
              .backgroundColor('#E0E0E0').fontColor('#333333')
              .borderRadius(20)
              .onClick(() => { this.isMaskVisible = false; })
            Button('确定').width(100).height(40)
              .backgroundColor('#3F6AE8').fontColor(Color.White)
              .borderRadius(20)
              .onClick(() => { this.isMaskVisible = false; })
          }.width('90%').justifyContent(FlexAlign.SpaceAround)
            .margin({ top: 24, bottom: 20 })
        }
        .width(300).backgroundColor(Color.White)
        .borderRadius(16)
        .shadow({ radius: 16, color: '#44000000' })
        .transition(
          TransitionEffect.asymmetric(
            TransitionEffect.translate({ x: 0, y: 80 })
              .combine(TransitionEffect.opacity(0))
              .animation({ duration: 350, curve: Curves.OutCubic }),
            TransitionEffect.opacity(0)
              .combine(TransitionEffect.scale({ x: 0.95, y: 0.95 }))
              .animation({ duration: 200, curve: Curves.InCubic })
          )
        )
      }
    }
    .width('100%').height('100%')
    .alignContent(Alignment.Center)
    .clip(false)
  }
}

@Component
struct CardContent {
  @Prop title: string = '';
  @Prop description: string = '';
  build() {
    Column() {
      Text(this.title).fontSize(17)
        .fontWeight(FontWeight.Medium).fontColor('#222222')
      Text(this.description).fontSize(14)
        .fontColor('#555555').lineHeight(22).margin({ top: 8 })
    }.width('100%').padding(16).backgroundColor(Color.White)
    .borderRadius(12).margin({ bottom: 12 })
    .shadow({ radius: 4, color: '#11000000' })
  }
}

五、核心技术解读

5.1 Stack 三层架构详解

本示例中 Stack 内包含三个 Z 序层次:

  • 第 1 层(主页面):始终存在,包含标题栏、滚动内容和底部按钮;
  • 第 2 层(遮罩):仅 isMaskVisible === true 时通过 if 条件渲染创建;
  • 第 3 层(弹窗):与遮罩同时出现,位于遮罩之上。

条件渲染的好处:弹窗未展示时不占用组件树节点;配合 .transition() 自动触发入场/出场动画。

5.2 .opacity() 的半透明效果

遮罩核心仅两行代码:

Rectangle()
  .fill(Color.Black)
  .opacity(0.5)   // 50% 不透明度
效果
0.0 完全透明
0.3 弱遮罩
0.5 中等(推荐)
0.7 强遮罩
1.0 完全不透明

5.3 .transition() 入场/出场动画

遮罩 — 对称过渡:透明度从 0 → 0.5(入场),0.5 → 0(出场),300ms 渐现渐隐。

弹窗 — 非对称过渡

  • 入场:从下方 80px 滑入 + 淡入,350ms,OutCubic 缓出曲线;
  • 出场:淡出 + 缩小至 95%,200ms,InCubic 缓入曲线。

5.4 @State 驱动状态

@State isMaskVisible: boolean = false;

用户点击"显示" → isMaskVisible = true → 条件渲染激活 → 遮罩 + 弹窗入场。
用户点击遮罩或按钮 → isMaskVisible = false → 出场动画 → 组件销毁。


六、最佳实践与扩展

6.1 封装可复用遮罩组件

@Component
export struct ModalOverlay {
  @Link isVisible: boolean;
  @BuilderParam content: () => void;

  build() {
    Stack() {
      if (this.isVisible) {
        Rectangle()
          .fill(Color.Black).opacity(0.5)
          .onClick(() => { this.isVisible = false; })
        Column() { this.content() }
          .width(320).backgroundColor(Color.White)
          .borderRadius(16)
      }
    }
    .width('100%').height('100%')
    .alignContent(Alignment.Center)
  }
}

6.2 扩展:模糊遮罩

除半透明纯色外,还可使用 .blur() 实现高斯模糊遮罩:

Column()
  .width('100%').height('100%')
  .backgroundColor('#33000000')
  .blur(12)    // 背景模糊 12px

6.3 性能要点

要点 说明
条件渲染 if 而非 .visibility(),隐藏时不占节点
动画时长 入场 250~350ms,出场 150~250ms
透明度 0.4~0.6 为宜,过暗产生压抑感
事件冒泡 遮罩 onClick 会拦截事件,无需额外处理

七、FAQ

Q1:遮罩没盖住状态栏?

设置 Stack 的 .expandSafeArea() 扩展到安全区域外。

Q2:TransitionEffect 不生效?

检查:过渡效果写在条件渲染的组件上而非父容器;.animation() 方法已调用;asymmetric() 参数顺序为入场在前、出场在后。

Q3:如何实现多层弹窗?

在 Stack 中继续追加条件渲染层即可,或使用栈结构管理多级弹窗:

@State maskStack: number[] = [];
get isAnyMaskVisible() { return this.maskStack.length > 0; }
pushDialog() { this.maskStack.push(Date.now()); }
popDialog() { this.maskStack.pop(); }

八、总结

本文通过完整的示例应用,详细讲解了鸿蒙 ArkTS 中利用 Stack 布局 + .opacity() + 条件渲染 + TransitionEffect 实现模态弹窗半透明遮罩的全过程。

核心要点:

技术 作用
Stack() Z 轴叠放容器,遮罩置于主内容之上
.opacity(0.5) 黑色矩形变为半透明蒙版
@State + if 条件渲染 控制遮罩/弹窗的创建与销毁
TransitionEffect 组件挂载/卸载时自动触发动画
alignContent(Center) 弹窗在 Stack 中居中定位

Stack 层叠布局是鸿蒙 ArkTS 最灵活的布局容器之一。掌握 Stack + 透明度 + 条件渲染 + 过渡动画的组合,可轻松应对模态弹窗、新手引导浮层、底部抽屉面板等场景。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐