【共创季稿事节】鸿蒙原生 ArkTS 布局方式之 Stack 实现遮罩层 / 半透明蒙版
鸿蒙原生 ArkTS 布局方式之 Stack 实现遮罩层 / 半透明蒙版



一、引言
在移动端应用中,模态弹窗是最常见的交互模式之一。一个标准的模态弹窗由两部分构成:
- 半透明遮罩层 — 覆盖在主内容之上,视觉上"变暗"背景,引导用户聚焦弹窗;
- 弹窗卡片 — 位于遮罩之上,承载操作内容。
在鸿蒙 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)?
原因有二:
- 语义清晰:
.opacity(0.5)明确表达"50% 透明度"的意图; - 动画自然:
.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 + 透明度 + 条件渲染 + 过渡动画的组合,可轻松应对模态弹窗、新手引导浮层、底部抽屉面板等场景。
更多推荐



所有评论(0)