【共创季稿事节】鸿蒙原生 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 数据模型定义
interfaceProduct{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 — 可复用的标签组件
@Componentstruct ProductTag{publictext:string='';publicbgColor:string='#FF0000';publicfontColor:string='#FFFFFF';publictagRadius: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{returnMath.round((1-this.product.discount)*100);}getDiscountedPrice():number{returnthis.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@Componentstruct Index{@StateproductList: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(){letprice=this.product.price*this.product.discount;// 编译错误Text(`${price}`)}// ✅ 正确写法getFinalPrice():number{returnthis.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 ms0 个编译错误,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接口,支持更多标签类型:
interfaceProduct{// ... 现有字段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。
