【共创季稿事节】鸿蒙原生 ArkTS 布局实战:使用 Stack 实现商品 Tag 标签叠加
鸿蒙原生 ArkTS 布局实战:使用 Stack 实现商品 Tag 标签叠加



一、背景与需求
在移动端电商应用中,商品卡片是最基础也最重要的信息载体。一个典型的商品卡片通常包含以下视觉层次:
- 商品图片 — 占据卡片主要区域,直观展示商品外观
- 标签信息 — 在图片之上叠加各种营销标签,如"新品"、“-30%”、“热卖”
- 价格信息 — 展示原价、折后价、促销价等
传统的前端布局方案实现"图片 + 标签叠加"通常需要 position: absolute 配合 z-index 来控制层级。而在鸿蒙 ArkUI 框架中,Stack 容器正是为解决此类"层叠布局"场景而设计的原生组件。
本文将从零开始,完整实现一个具备 4 种不同标签组合的商品卡片展示页面,并深入解析 Stack 布局的核心机制与 ArkTS 开发中的关键注意事项。
二、Stack 布局核心概念
2.1 什么是 Stack
Stack 是 ArkUI 提供的一种堆叠容器,其内部子组件按照书写顺序从底层到顶层依次堆叠。也就是说:
- 第一个子组件位于最底层(Z 轴的最下方)
- 最后一个子组件位于最顶层(Z 轴的最上方)
- 上层组件会覆盖下层组件的重叠区域
这与 Web 开发中 position: relative 容器内放置 position: absolute 子元素的思路类似,但 ArkUI 的 Stack 是原生组件,性能更优、语义更清晰。
2.2 Stack 的核心属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
align |
Alignment |
Alignment.Center |
控制未显式定位的子组件在 Stack 内的对齐方式 |
width |
Length |
自适应 | 容器宽度 |
height |
Length |
自适应 | 容器高度 |
2.3 子组件定位方式
Stack 中的子组件有两种定位策略:
策略一:通过父容器的 align 属性统一对齐
在子组件上使用 .align(Alignment.TopStart) 将其定位到 Stack 的左上角。这种方式适用于"标签栏"这类整体需要对齐到某个边缘的区域。
策略二:通过 position 属性绝对定位
在子组件上使用 .position({ top: 8, right: 8 }) 或 .position({ bottom: 0 }) 进行精确的像素级定位。这种方式适用于"折扣标签"、"价格栏"等需要固定在某个具体位置的元素。
值得注意的是,.align() 和 .position() 可以混合使用在同一个 Stack 的不同子组件上,这为复杂布局提供了极大的灵活性。
三、项目架构与组件设计
3.1 整体组件树
Index (页面入口)
├── Scroll (可滚动容器)
│ └── Column (垂直布局)
│ ├── 页面标题
│ ├── 副标题
│ ├── ForEach (遍历商品列表)
│ │ └── ProductCard (商品卡片)
│ │ └── Stack (核心堆叠区域)
│ │ ├── 商品图片 (底层)
│ │ ├── 标签区域 (左上角)
│ │ ├── 折扣标签 (右上角)
│ │ └── 价格栏 (底部)
│ └── LayoutTipsCard (布局知识卡片)
│ └── TipRow × 5 (提示条目)
3.2 组件职责划分
整个页面共拆分为 5 个自定义组件和 1 个数据接口:
| 组件 / 接口 | 职责 | 复用性 |
|---|---|---|
Product (interface) |
定义商品数据结构 | 全局数据模型 |
ProductTag |
封装单个标签的外观 | 可复用(新品/热卖/折扣) |
ProductCard |
实现 Stack 核心堆叠逻辑 | 可复用(任意商品) |
LayoutTipsCard |
展示布局知识点 | 一次性提示 |
TipRow |
单行提示条目 | 可复用 |
Index |
页面入口 + 数据提供 | 页面级 |
这种组件化的设计遵循了 ArkUI 的推荐实践:每个组件只关注自己的职责,数据通过属性(public 字段)从父组件流向子组件。
四、代码深度解析
4.1 数据模型定义
interface Product {
name: string; // 商品名称
price: number; // 商品价格(元)
discount: number; // 折扣比例(0.7 = 7折,0 = 无折扣)
isNew: boolean; // 是否为新品
isHot: boolean; // 是否为热卖款
bgColor: string; // 背景色(十六进制字符串)
icon: string; // 商品图标(Emoji 模拟图片)
}
这里有一个需要注意的 ArkTS 特性:颜色值建议使用 string 类型(如 '#F44336'),而不是 Color 枚举。因为在数据列表中我们通常从 JSON 或服务端获取颜色字符串,而 Color 枚举只能表示有限的几个预设值(Color.Red、Color.Green 等),无法表示任意十六进制颜色。ArkUI 的 .backgroundColor() 方法同时接受 string 和 Color 类型,所以使用 string 更灵活。
4.2 ProductTag — 可复用的标签组件
@Component
struct ProductTag {
public text: string = '';
public bgColor: string = '#FF0000';
public fontColor: string = '#FFFFFF';
public tagRadius: number = 8;
build() {
Text(this.text)
.fontSize(12)
.fontColor(this.fontColor)
.fontWeight(FontWeight.Bold)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor(this.bgColor)
.borderRadius(this.tagRadius)
}
}
关键设计决策:
- 属性用
public而非private:这是 ArkTS 的重要约束 — 父组件通过构造器语法ProductTag({ text: '...', bgColor: '...' })传入的属性必须声明为public。如果用private修饰,编译时会报 “Property ‘text’ is private and can not be initialized through the component constructor” 警告。 - 属性名避免与系统方法冲突:这里使用
tagRadius而不是borderRadius,因为borderRadius是CommonAttribute上的链式方法名。如果定义一个同名的public属性,编译器会报类型冲突错误"Property ‘borderRadius’ in type ‘ProductTag’ is not assignable to the same property in base type ‘CustomComponent’"。 - 提供合理的默认值:每个属性都赋予默认值,这样即使父组件未传入某些属性,子组件也能正常渲染。
4.3 ProductCard — Stack 布局的核心实现
这是整个示例最核心的组件,一个 Stack 内包含 4 个堆叠层:
第 1 层:商品图片(底层)
// 第 1 层(底层):模拟商品图片区域
Column() {
Text(this.product.icon).fontSize(72)
Text('商品示意图').fontSize(14).fontColor(Color.White)
}
.width('100%').height(200)
.backgroundColor(this.product.bgColor)
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
这是 Stack 中第一个子组件,因此位于 Z 轴的最底层。在实际项目中,这里会替换为 Image 组件加载真实的商品图片。本示例使用 Emoji + 色块来模拟图片区域,方便读者在不依赖图片资源的情况下运行和调试。
第 2 层:新品 / 热卖标签(左上角)
// 第 2 层(顶层左侧):新品 / 热卖 标签
Column() {
if (this.product.isNew) {
ProductTag({ text: '✨ 新品', bgColor: '#4CAF50', ... })
.margin({ bottom: 6 })
}
if (this.product.isHot) {
ProductTag({ text: '🔥 热卖', bgColor: '#FF9800', ... })
}
}
.align(Alignment.TopStart) // ← 关键定位
.padding({ left: 8, top: 8 })
定位要点:.align(Alignment.TopStart) 将整个标签列定位到 Stack 的左上角。Alignment 枚举支持 9 个方位(TopStart、TopCenter、TopEnd、Start、Center、End、BottomStart、BottomCenter、BottomEnd),可以满足绝大多数对齐需求。
条件渲染:通过 if (this.product.isNew) 和 if (this.product.isHot) 实现标签的按需显示。这种声明式的条件渲染比命令式的 visibility 切换更符合 ArkUI 的设计理念。
第 3 层:折扣标签(右上角)
// 第 3 层(顶层右侧):折扣标签
if (this.product.discount > 0) {
ProductTag({
text: `-${this.getDiscountPercent()}%`,
bgColor: '#F44336',
...
})
.position({ top: 8, right: 8 }) // ← 绝对定位到右上角
}
定位要点:使用 .position({ top: 8, right: 8 }) 进行绝对定位。position 的坐标相对于父容器(Stack)的左上角。这里我们将折扣标签固定在右上角,与左上角的新品标签形成视觉对称。
注意:position 会使元素脱离 Stack 的默认流式布局,不会影响其他子组件的位置。这也是为什么折扣标签和标签列可以分别定位在右上角和左上角而互不干扰。
第 4 层:名称和价格栏(底部)
// 第 4 层(底部):商品名称和价格
Row() {
Text(this.product.name).fontSize(16).fontColor(Color.White)
Blank()
// ... 价格显示逻辑
}
.position({ bottom: 0 }) // ← 固定在底部
.width('100%')
.backgroundColor(Color.Gray).opacity(0.85)
.borderRadius({ bottomLeft: 12, bottomRight: 12 })
定位要点:.position({ bottom: 0 }) 将价格栏固定在 Stack 的底部。这里有一个巧妙的设计:价格栏设置了 .opacity(0.85) 半透明背景,让底部的商品图片可以隐约透出,增加视觉层次感。
Stack 容器配置
Stack() {
// ... 4 个子层
}
.width('100%')
.height(200)
.borderRadius(12)
Stack 容器本身设置了固定的高度(200vp)和圆角(12vp),其内部的子组件通过 width('100%') 和 height('100%') 来继承容器的尺寸,确保各层能够完全覆盖。
4.4 计算逻辑提取
// 在 ProductCard 组件中
getDiscountPercent(): number {
return Math.round((1 - this.product.discount) * 100);
}
getDiscountedPrice(): number {
return this.product.price * this.product.discount;
}
为什么要提取为方法? 这是 ArkTS 的一个重要约束:在 build() 方法的 UI 描述区内,不允许声明 let / const 变量。如果直接在 build() 中写 let discountPercent = ...,编译会报 “Only UI component syntax can be written here” 错误。
因此,所有计算逻辑都必须提取为组件的方法(或者使用 getter 属性),在 build() 中通过 this.getDiscountPercent() 的形式调用。
4.5 页面入口与数据驱动
@Entry
@Component
struct Index {
@State productList: Product[] = [
{ name: '北欧风沙发', price: 2999, discount: 0.7, isNew: true, isHot: false, ... },
{ name: '智能手表 Pro', price: 1299, discount: 0, isNew: true, isHot: true, ... },
{ name: '无线降噪耳机', price: 899, discount: 0.8, isNew: false, isHot: true, ... },
{ name: '极简台灯', price: 199, discount: 0.55, isNew: false, isHot: false, ... },
];
build() {
Scroll() {
Column() {
// 标题
// 商品列表 → ForEach
// 布局提示
}
.constraintSize({ minHeight: '100%' })
}
}
}
数据驱动:通过 @State 装饰器声明商品列表数据,实现响应式更新。当 productList 的内容发生变化时,框架会自动重新渲染 UI。
ForEach 遍历:使用 ForEach 遍历渲染商品卡片,每次迭代创建一个 ProductCard 实例并通过 { product: item } 传入数据。
constraintSize({ minHeight: '100%' }):这是替代 minHeight 的 ArkTS API 写法。Column 组件没有直接的 minHeight 属性,需要通过 constraintSize 来设置最小尺寸约束。
五、ArkTS 开发的 8 个关键注意事项
通过这个示例的编写和调试过程,我总结了以下 ArkTS 开发中容易踩坑的要点:
5.1 组件属性必须用 public
规则:父组件通过构造器语法 ChildComponent({ prop: value }) 传入的属性,在子组件中必须声明为 public。
原因:ArkTS 的类型系统和编译期检查要求构造器初始化的目标属性可公开访问。private 属性只能在组件内部访问,不能通过外部构造器写入。
例外:@State、@Prop、@Link 等装饰器修饰的属性不受此限制,它们有自己的数据传递机制。
5.2 避免属性名与系统方法冲突
规则:组件中声明 public 属性时,避免使用与 CommonAttribute 同名的方法名(如 borderRadius、width、height、padding 等)。
原因:这些方法名在编译时会被视为对基类方法的覆盖(override),造成类型签名不匹配的错误。
最佳实践:给属性名加上业务前缀,如 tagRadius、cardWidth、contentPadding 等。
5.3 build() 方法内不能声明变量
规则:build() 方法的直接代码块中(UI 描述区),只能包含组件构造和链式调用,不能出现 let、const 声明。
原因:ArkUI 的声明式 UI 语法要求在 build() 中只能描述 UI 结构,任何计算逻辑都应提取到方法或计算属性中。
解决方案:将计算逻辑提取为组件方法:
// ❌ 错误写法
build() {
let price = this.product.price * this.product.discount; // 编译错误
Text(`${price}`)
}
// ✅ 正确写法
getFinalPrice(): number {
return this.product.price * this.product.discount;
}
build() {
Text(`${this.getFinalPrice()}`)
}
5.4 颜色值使用 string 而非 Color
规则:如果需要灵活的颜色值(尤其是从数据或配置中读取),使用 string 类型(如 '#FF0000'、'rgb(255,0,0)')而不是 Color 枚举。
原因:Color 枚举只包含有限的预设值(Color.Red、Color.Green、Color.Blue 等),无法表达任意十六进制颜色。而 .backgroundColor()、.fontColor() 等 API 同时接受 string 和 Color 类型。
5.5 使用 constraintSize 替代 minHeight
规则:在 Column 或 Row 上设置最小高度时,使用 .constraintSize({ minHeight: '100%' }) 而非 .minHeight('100%')。
原因:ArkUI 的 Column 属性集中没有直接的 minHeight 属性,需要通过 constraintSize 对象来设置尺寸约束。
5.6 Stack 的子组件顺序决定 Z 轴层级
规则:Stack 内先写的子组件在底层,后写的在顶层。
常见误区:很多开发者会误以为后写的在底层(类似 CSS 的 z-index 思维)。实际上,Stack 的层级顺序是先写 = 底层,后写 = 顶层,与 HTML 的默认堆叠顺序一致。
5.7 align 与 position 的适用场景
规则:
- 当子组件是一个容器(如 Column),内部包含多个元素需要整体定位时,使用
.align(Alignment.Xx) - 当子组件是单个元素,需要精确的像素级定位时,使用
.position({ top, right, bottom, left })
示例:
- 新品/Hot 标签列 →
.align(Alignment.TopStart)— 整体对齐到左上角 - 折扣标签 →
.position({ top: 8, right: 8 })— 精确固定在右上角 - 价格栏 →
.position({ bottom: 0 })— 精确固定在底部
5.8 使用 ForEach 遍历列表时注意键值
规则:ForEach 的第三个参数可以指定键值生成函数,帮助框架高效识别哪个列表项发生了变化。
ForEach(this.productList, (item: Product) => {
ProductCard({ product: item })
}, (item: Product) => item.name) // 以 name 作为唯一键
虽然本示例未显式传入键值函数(使用了默认索引),但在实际生产项目中,建议为列表项提供稳定的唯一键以优化 diff 性能。
六、运行效果与验证
6.1 编译验证
在项目根目录执行以下命令:
hvigorw assembleApp --no-daemon
编译结果:
BUILD SUCCESSFUL in 8 s 556 ms
0 个编译错误,0 个代码警告。
6.2 预期运行效果
页面启动后将展示:
- 页面标题:“🛍️ 商品展示 — Stack + Tag 标签叠加”
- 4 张商品卡片,每张卡片展示不同的标签组合:
| 商品 | 折扣 | 标签 | 背景色 |
|---|---|---|---|
| 🛋️ 北欧风沙发 | -30% | ✨ 新品 | 墨绿 |
| ⌚ 智能手表 Pro | 无折扣 | ✨ 新品 + 🔥 热卖 | 深蓝 |
| 🎧 无线降噪耳机 | -20% | 🔥 热卖 | 紫色 |
| 💡 极简台灯 | -45% | 无标签 | 暖木色 |
- 布局要点提示卡片:展示 Stack 布局的 5 个核心技巧
6.3 交互验证
- 页面支持纵向滚动,可浏览所有商品卡片
- 每个卡片的标签根据数据自动按需显示
- 带折扣的商品同时显示原价(带删除线)和折后价
七、扩展与优化方向
7.1 替换为真实图片
将第 1 层的 Emoji 图标替换为 Image 组件:
Image(this.product.imageUrl)
.objectFit(ImageFit.Cover) // 图片裁剪方式
.width('100%')
.height('100%')
7.2 添加动画效果
为标签添加入场动画,提升用户体验:
ProductTag({ ... })
.transition({ type: TransitionType.Insert, scale: { x: 0, y: 0 } })
7.3 支持更多标签类型
扩展 Product 接口,支持更多标签类型:
interface Product {
// ... 现有字段
tags: string[]; // 自定义标签数组
badges: Badge[]; // 角标列表
}
7.4 响应式适配
根据屏幕尺寸动态调整卡片大小和标签位置,使用 breakpoint 或 mediaQuery:
if (this.isWideScreen()) {
// 平板:大卡片 + 横向布局
} else {
// 手机:小卡片 + 网格布局
}
八、总结
本文通过一个完整的电商商品卡片案例,深入解析了 HarmonyOS NEXT 中 Stack 布局的使用方法和 ArkTS 开发的核心要点。
核心收获:
- Stack 的核心价值:在同一区域叠加多层 UI 元素,通过
.align()和.position()灵活控制每层位置 - Z 轴层级规则:Stack 内子组件按书写顺序从底到顶堆叠
- 组件化设计:将标签封装为独立组件
ProductTag,提高代码复用性 - ArkTS 语法约束:了解
public属性、避免命名冲突、build() 内禁止声明变量等关键规则 - 数据驱动 UI:通过
@State+ForEach实现列表渲染和响应式更新
Stack 布局是鸿蒙 ArkUI 中最常用也最强大的容器之一,掌握它的使用方式和最佳实践,可以轻松应对各种"层叠叠加"的 UI 场景——不仅仅是商品标签,还包括头像叠加、通知角标、遮罩层、工具提示(Tooltip)、模态引导等。
希望本文能帮助你更好地理解和使用鸿蒙原生 Stack 布局,在实际项目中打造出既美观又高性能的叠加 UI。
更多推荐


所有评论(0)