API 版本:HarmonyOS NEXT 6.1.1 (API 24)
目标读者:HarmonyOS 应用开发者,希望深入理解 ArkUI 布局系统的技术人员
前置知识:ArkTS 基础语法、ArkUI 声明式 UI 开发范式


项目演示

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

目录


第一章:引言

HarmonyOS NEXT 的发布标志着移动应用开发进入了一个全新的时代。作为首个完全基于鸿蒙内核的操作系统,它彻底摆脱了 Android 的历史包袱,构建了从芯片到应用的全栈自主可控的技术体系。而 ArkUI 作为 HarmonyOS 的声明式 UI 框架,其布局系统是整个应用开发的基石。

在 ArkUI 的布局体系中,Column、Row 和 Stack 是最基础也是最重要的三个容器组件。它们分别代表了两种截然不同的布局哲学——线性排列层叠覆盖。正确理解和选用这三种容器,是构建出高性能、可维护、视觉效果出色应用的前提。

1.1 一个真实的开发困境

假设你正在开发一个社交媒体应用,需要实现"头像 + 在线状态指示器"的功能。初看起来这很简单,但不同的实现方式会带来截然不同的结果。

方案 A:用 Column/Row 嵌套实现

Row() {
  Column() {
    Image($r('app.media.avatar'))
      .width(60)
      .height(60)
      .borderRadius(30)
  }
  
  Text('●')
    .fontSize(12)
    .fontColor('#4CAF50')
    .margin({ left: -12, top: 12 })
}

这个方案虽然能运行,但存在三个严重问题:

  1. 脆弱的负偏移margin({ left: -12, top: 12 }) 是一个硬编码的"魔法数字"。如果头像大小从 60 变为 80,偏移量需要重新计算
  2. 视觉错位风险:在不同屏幕密度下,像素对齐可能出现偏差
  3. 维护困难:后续如果想调整指示器位置,需要反复试错调整 margin 值

方案 B:用 Stack 实现

Stack({ alignContent: Alignment.Center }) {
  Image($r('app.media.avatar'))
    .width(60)
    .height(60)
    .borderRadius(30)
  
  Text('●')
    .fontSize(12)
    .fontColor('#4CAF50')
    .position({ x: 48, y: 48 })
}
.width(60)
.height(60)

方案 B 的优势一目了然:

  1. 语义清晰:Stack 天然表达"层叠"的意图,代码即文档
  2. 位置独立:指示器的位置相对于 Stack 容器本身,而非依赖其他组件的尺寸
  3. 易于调整:修改头像大小不需要同步修改指示器位置

这个简单的例子揭示了一个深刻的道理:选择正确的布局容器,不仅是技术问题,更是设计理念的体现

1.2 本文的价值

在 HarmonyOS 开发社区中,关于 Column 和 Row 的教程已经很多,但对 Stack 的深入讲解相对较少。许多开发者虽然"会用" Stack,但对它的工作原理、最佳实践、以及何时应该(或不应该)使用 Stack 缺乏系统认识。

本文试图填补这一空白,通过理论分析与实战案例相结合的方式,帮助读者:

  • 深入理解三种容器的底层工作机制
  • 掌握 Column/Row 的适用场景与局限性
  • 精通 Stack 的所有核心特性
  • 建立起清晰的布局选型决策模型
  • 学习 API 24 带来的最新能力

第二章:布局容器基础概念

在深入探讨 Stack 之前,我们需要先建立对 ArkUI 布局系统的整体认知。

2.1 ArkUI 布局系统概述

ArkUI 的布局系统是一套声明式、组件化的布局框架。与传统的命令式布局(如 XML + Java 代码)不同,ArkUI 采用"UI 是状态的函数"这一核心理念:开发者只需要声明 UI 结构和样式,框架会自动处理布局计算和渲染。

ArkUI 的布局容器可以分为两大类:

线性布局容器(Linear Layout Containers)

  • Column:垂直方向排列子组件
  • Row:水平方向排列子组件
  • Flex:灵活盒布局,支持换行和对齐控制
  • List:高性能列表容器
  • Grid:网格布局容器

层叠/绝对布局容器(Stack/Absolute Layout Containers)

  • Stack:Z 轴层叠容器
  • FolderStack:折叠屏适配的 Stack(API 11+)

2.2 坐标系与布局方向

理解布局方向是区分不同容器的关键。ArkUI 使用标准的屏幕坐标系:

原点 (0, 0) ────────────→ X 轴向右
    │
    │
    │
    ↓
   Y 轴向下

不同容器的"主轴"方向不同:

  • Column:主轴是 Y 轴(垂直向下),交叉轴是 X 轴(水平)
  • Row:主轴是 X 轴(水平向右),交叉轴是 Y 轴(垂直)
  • Stack:没有传统意义上的主轴/交叉轴,所有子组件共享同一个位置,形成 Z 轴层叠

2.3 三种核心布局容器的定位

可以用一个简单的比喻来理解三种容器:

容器 比喻 特点
Column 垂直书架 书一本接一本竖直排列
Row 横向排列的士兵 士兵们肩并肩站立
Stack 叠起来的文件 文件一张盖在另一张上面

这个比喻揭示了最核心的差异:

  • Column 和 Row 是"排列关系"——子组件之间有空间占用关系,彼此独立不重叠
  • Stack 是"层叠关系"——所有子组件共享同一个空间位置,通过 Z 序决定显示层级

第三章:Column/Row 线性布局详解

Column 和 Row 是 ArkUI 中最常用的容器组件。掌握它们是高效开发 HarmonyOS 应用的基础。

3.1 Column 容器基础

Column 是垂直方向的线性布局容器,子组件从上到下依次排列。

Column() {
  Text('第一个子组件')
    .fontSize(16)
  Text('第二个子组件')
    .fontSize(14)
  Button('第三个子组件')
}
.width('100%')
.padding(16)

3.2 Row 容器基础

Row 是水平方向的线性布局容器,子组件从左到右依次排列。

Row() {
  Icon($r('app.media.ic_star'))
  Text('收藏')
    .fontSize(14)
    .margin({ left: 4 })
}
.padding(8)

3.3 核心属性全解析

3.3.1 justifyContent - 主轴对齐

控制子组件在主轴方向的分布方式。

Column 的主轴是垂直方向

Column() {
  Text('顶部对齐')
  Text('内容')
}
.width(200)
.height(300)
.justifyContent(FlexAlign.Start)  // 垂直靠上

Column() {
  Text('居中对齐')
  Text('内容')
}
.width(200)
.height(300)
.justifyContent(FlexAlign.Center) // 垂直居中

Column() {
  Text('底部对齐')
  Text('内容')
}
.width(200)
.height(300)
.justifyContent(FlexAlign.End)    // 垂直靠下

所有可用的 FlexAlign 值:

  • Start:主轴起始位置
  • Center:主轴居中
  • End:主轴结束位置
  • SpaceBetween:两端对齐,中间均分
  • SpaceAround:每个组件周围均分空间
  • SpaceEvenly:所有间距相等
3.3.2 alignItems - 交叉轴对齐

控制子组件在交叉轴方向的对齐方式。

Column 的交叉轴是水平方向

Column() {
  Text('左对齐')
  Text('内容')
}
.width(200)
.alignItems(HorizontalAlign.Start) // 水平靠左

Column() {
  Text('居中对齐')
  Text('内容')
}
.width(200)
.alignItems(HorizontalAlign.Center) // 水平居中
3.3.3 layoutWeight - 弹性布局

layoutWeight 是 Column/Row 中最强大的特性之一,实现了类似 Flexbox 的弹性布局。

Row() {
  Text('固定宽度 100')
    .width(100)
  Text('剩余空间全部占据')
    .layoutWeight(1)
  Text('占据剩余空间的一半')
    .layoutWeight(1)
}
.width('100%')

权重计算规则:剩余空间 = 容器总宽度 - 固定宽度组件的总宽度。各组件按 layoutWeight 值在剩余空间中分配。

3.4 使用场景与局限性

3.4.1 典型使用场景

Column/Row 是最通用的布局容器,适合绝大多数线性排列场景:

  1. 表单页面:标签和输入框垂直排列
  2. 列表项:图标、标题、描述垂直/水平排列
  3. 按钮组:多个按钮水平并排
  4. 卡片布局:图片在上,文字在下
3.4.2 局限性

虽然 Column/Row 功能强大,但它们有一个根本性的限制——子组件无法重叠

这个限制带来了一些挑战:

挑战一:实现"浮在上面"的效果

// 实现"购买按钮"浮在商品图片的右下角
Row() {
  Column() {
    Image($r('app.media.product'))
      .width(200)
      .height(200))
  }
  Button('购买')
  // 这里无法让按钮"浮在"图片上
}

挑战二:实现遮罩效果

// 在内容上方覆盖半透明遮罩
Column() {
  Scroll() { /* 大量内容 */ }
  // 遮罩层只能在内容"下方",无法覆盖
}

这些局限性正是 Stack 发挥价值的地方。当你遇到上述需求时,就应该考虑使用 Stack 了。


第四章:Stack 层叠布局详解

Stack 是 ArkUI 中最独特的布局容器,它打破了传统布局"子组件不重叠"的规则,让多个子组件可以在同一位置层叠显示。

4.1 Stack 工作原理

4.1.1 Z 轴层叠模型

在传统的二维布局中,我们只有 X 轴(水平)和 Y 轴(垂直)。Stack 引入了第三个维度——Z 轴,代表"深度"或"层级"。

Z 轴(指向屏幕外)
  ↑
  │  ┌─────────┐  ← 顶层(最新声明)
  │  │ Layer3 │
  │  ├─────────┤
  │  │ Layer2 │  ← 中层
  │  ├─────────┤
  │  │ Layer1 │  ← 底层(最早声明)
  │  └─────────┘
  └──────────────→ X 轴
      ↓
   Y 轴
4.1.2 声明顺序即层级顺序

Stack 的层叠规则非常简单:后声明的子组件在上层

Stack() {
  Text('底层')
    .width(200)
    .height(200)
    .backgroundColor('#2196F3')
  
  Text('中层')
    .width(150)
    .height(150)
    .backgroundColor('#FF9800')
  
  Text('顶层')
    .width(100)
    .height(100)
    .backgroundColor('#4CAF50')
}
.width(250)
.height(250)

4.2 alignContent 对齐方式

alignContent 是 Stack 唯一的构造参数,用于设置所有子组件的默认对齐方式。

九种对齐方式:

  • TopStart / Top / TopEnd(顶部三档)
  • Start / Center / End(中间三档)
  • BottomStart / Bottom / BottomEnd(底部三档)
// 左上角对齐
Stack({ alignContent: Alignment.TopStart }) { ... }

// 正中心对齐(默认值)
Stack({ alignContent: Alignment.Center }) { ... }

// 右下角对齐
Stack({ alignContent: Alignment.BottomEnd }) { ... }

4.3 Z 序控制

虽然默认的层叠规则是"后声明在上层",但 zIndex 属性允许我们手动控制层叠顺序。

Stack() {
  // 后声明但 zIndex 小 → 在底层
  Text('我在下面')
    .width(150)
    .height(150)
    .backgroundColor('#2196F3')
    .zIndex(1)
  
  // 先声明但 zIndex 大 → 在上层
  Text('我在上面')
    .width(100)
    .height(100)
    .backgroundColor('#4CAF50')
    .zIndex(10)
}
.width(200)
.height(200)

规则总结

  • zIndex 值越大,层级越高
  • 可以使用负数
  • 相同 zIndex 时回退到"后声明在上层"的默认规则
  • 仅在同一个 Stack 容器内有效

4.4 子组件定位方式

Stack 中的子组件有三种定位方式:

4.4.1 默认定位:alignContent + margin
Stack({ alignContent: Alignment.BottomEnd }) {
  Text('右下角')
    .padding(8)
    .backgroundColor('#E3F2FD')
  Text('偏移后')
    .padding(8)
    .backgroundColor('#FFEBEE')
    .margin({ right: 50, bottom: 50 })
}
.width(200)
.height(200)
4.4.2 offset:视觉偏移

offset 让组件只做视觉位置的偏移,不影响布局计算。

Stack({ alignContent: Alignment.Center }) {
  Text('原始位置')
    .padding(8)
    .backgroundColor('#E3F2FD')
  Text('offset 偏移')
    .padding(8)
    .backgroundColor('#FFEBEE')
    .offset({ y: -20 })
}
.width(200)
.height(200)
4.4.3 position:绝对定位

position 是最精确的定位方式,让组件"脱离"布局流。

Stack() {
  Column()
    .width('100%')
    .height('100%')
    .backgroundColor('#E3F2FD')
  
  Text('左上角')
    .position({ x: 16, y: 16 })
  
  Text('右下角')
    .position({ x: 130, y: 160 })
}
.width(200)
.height(200)

第五章:Stack vs Column/Row 核心对比

5.1 本质差异:线性 vs 层叠

Column/Row 和 Stack 的根本差异在于它们对"子组件空间关系"的理解完全不同。

Column/Row 的空间模型
每个子组件占据独立的空间区域,彼此之间有清晰的边界。子组件的位置是"相对"的——取决于前一个组件在哪里结束。

Stack 的空间模型
所有子组件共享同一个空间区域。子组件的位置是"绝对"的——相对于 Stack 容器本身,而非其他子组件。

5.2 布局能力对比表

对比维度 Column/Row Stack
布局方向 垂直/水平线性 Z 轴层叠
子组件是否重叠 不支持 完全支持
默认对齐 主轴起始位置 容器中心
弹性布局 layoutWeight 不支持
主轴/交叉轴 有明确概念 无此概念
空间占用 独立空间 共享空间
组件相互影响 有影响 独立
绝对定位 需要 offset/margin 原生支持 position
Z 序控制 不适用 zIndex 属性
响应式适配 天然支持 需手动处理

5.3 为什么不能用 Column/Row 替代 Stack?

5.3.1 案例一:圆形头像 + 在线状态

Stack 方案

Stack({ alignContent: Alignment.Center }) {
  Image($r('app.media.avatar'))
    .width(60)
    .height(60)
    .borderRadius(30)
  Column()
    .width(12)
    .height(12)
    .borderRadius(6)
    .backgroundColor('#4CAF50')
    .position({ x: 48, y: 48 })
}
.width(60)
.height(60)

对比 Column/Row 方案,Stack 方案在可读性、可维护性、扩展性上全面占优。

5.3.2 案例二:半透明遮罩层

需要在页面内容上覆盖半透明遮罩(如 Loading)。这是 Column/Row 根本做不到的——因为遮罩层会出现在内容下方,而不是覆盖在上方。

// 只有 Stack 能实现
Stack() {
  Scroll() { /* 主内容 */ }
    .width('100%')
    .height('100%')
  
  if (this.isLoading) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('#80000000')
    LoadingProgress()
  }
}
5.3.3 案例三:图片上的渐变遮罩文字

需要在图片底部叠加渐变,上面显示标题。这是多元素层叠场景,只有 Stack 能优雅实现。

Stack({ alignContent: Alignment.BottomStart }) {
  Image($r('app.media.photo'))
    .width('100%')
    .height(200)
  
  Column()
    .width('100%')
    .height(100)
    .linearGradient({
      direction: GradientDirection.Bottom,
      colors: [['#00000000', 0], ['#99000000', 1]]
    })
  
  Column() {
    Text('标题文字')
      .fontSize(18)
      .fontColor('#FFFFFF')
  }
  .padding(16)
}
.width('100%')
.height(200)

5.4 什么时候必须用 Stack?

通过上述案例,我们可以总结出必须使用 Stack 的几类场景:

5.4.1 徽章/角标类
  • 头像 + 在线状态点
  • 图标 + 消息未读数
  • 商品图片 + "热"标签
5.4.2 遮罩覆盖类
  • Loading 状态遮罩
  • 模态弹窗背景
  • 图片浏览深色背景
5.4.3 渐变叠加类
  • 图片 + 渐变 + 文字
  • 进度条填充 + 背景
  • 卡片边框 + 背景 + 内容
5.4.4 浮层导航类
  • 地图上的定位按钮
  • 页面右下角的返回顶部按钮
  • 购物车悬浮按钮
5.4.5 过渡动画类
  • 卡片堆叠切换
  • Tab 页内容切换动画
  • 展开/收起效果

第六章:实战案例解析

理论是基础,但真正的理解来自实战。本章通过五个典型案例,深入展示 Stack 的使用方法和最佳实践。

6.1 案例一:带徽标的用户头像组件

需求描述

实现一个可复用的用户头像组件,支持显示头像、在线状态、未读消息数。

完整代码
@Component
struct UserAvatar {
  @Prop avatarUrl: string = ''
  @Prop userName: string = '用户'
  @Prop unreadCount: number = 0
  @Prop isOnline: boolean = false
  @Prop size: number = 60

  build() {
    Column() {
      Stack({ alignContent: Alignment.Center }) {
        // 底层:头像
        if (this.avatarUrl) {
          Image(this.avatarUrl)
            .width(this.size)
            .height(this.size)
            .borderRadius(this.size / 2)
            .objectFit(ImageFit.Cover)
        } else {
          Column()
            .width(this.size)
            .height(this.size)
            .borderRadius(this.size / 2)
            .backgroundColor('#E0E0E0')
            .justifyContent(FlexAlign.Center) {
            Text(this.userName.substring(0, 1))
              .fontSize(this.size / 2)
              .fontColor('#9E9E9E')
          }
        }

        // 右上角:未读消息徽章
        if (this.unreadCount > 0) {
          this.UnreadBadge()
            .position({
              x: this.size - this.badgeSize() / 2 - 4,
              y: -this.badgeSize() / 2 + 4
            })
        }

        // 右下角:在线状态
        if (this.isOnline) {
          Column()
            .width(14)
            .height(14)
            .borderRadius(7)
            .backgroundColor('#4CAF50')
            .borderWidth(2)
            .borderColor('#FFFFFF')
            .position({
              x: this.size - 14 - 2,
              y: this.size - 14 - 2
            })
        }
      }
      .width(this.size)
      .height(this.size)

      Text(this.userName)
        .fontSize(12)
        .fontColor('#333333')
        .margin({ top: 4 })
    }
    .alignItems(HorizontalAlign.Center)
  }

  badgeSize(): number {
    if (this.unreadCount >= 100) {
      return 20
    } else if (this.unreadCount >= 10) {
      return 18
    } else {
      return 16
    }
  }

  @Builder
  UnreadBadge() {
    Row() {
      Text(this.unreadCount >= 99 ? '99+' : this.unreadCount.toString())
        .fontSize(10)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)
    }
    .height(this.badgeSize())
    .padding({ left: 4, right: 4 })
    .constraintSize({ minWidth: this.badgeSize() })
    .backgroundColor('#FF5722')
    .borderRadius(this.badgeSize() / 2)
    .justifyContent(FlexAlign.Center)
  }
}
使用示例
@Entry
@Component
struct AvatarDemo {
  build() {
    Row({ space: 24 }) {
      UserAvatar({
        avatarUrl: 'https://example.com/avatar1.jpg',
        userName: '张三',
        unreadCount: 3,
        isOnline: true,
        size: 60
      })

      UserAvatar({
        userName: '李四',
        unreadCount: 15,
        size: 60
      })

      UserAvatar({
        avatarUrl: 'https://example.com/avatar3.jpg',
        userName: '王五',
        unreadCount: 128,
        isOnline: true,
        size: 80
      })
    }
    .padding(24)
    .backgroundColor('#F5F5F5')
  }
}

6.2 案例二:商品卡片 - 多层叠效果

需求描述

实现一个商品推荐卡片,包含商品图片、标签、渐变遮罩、商品名称和价格。

完整代码
@Component
struct ProductCard {
  @Prop productName: string
  @Prop price: number
  @Prop originalPrice: number = 0
  @Prop tag: string = ''

  build() {
    Column() {
      // 图片区域:Stack 实现多层叠
      Stack({ alignContent: Alignment.BottomStart }) {
        // 底层:商品图片
        Column()
          .width('100%')
          .height(180)
          .linearGradient({
            direction: GradientDirection.BottomRight,
            colors: [['#2196F3', 0], ['#1565C0', 1]]
          })

        // 标签
        if (this.tag) {
          Text(this.tag)
            .fontSize(10)
            .fontColor('#FFFFFF')
            .padding({ left: 8, right: 8, top: 4, bottom: 4 })
            .backgroundColor('#FF5722')
            .borderRadius(4)
            .position({ x: 12, y: 12 })
        }

        // 渐变遮罩
        Column()
          .width('100%')
          .height(80)
          .linearGradient({
            direction: GradientDirection.Bottom,
            colors: [['#00000000', 0], ['#66000000', 1]]
          })

        // 商品名称
        Text(this.productName)
          .fontSize(14)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Medium)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .width('100%')
          .padding({ left: 12, right: 12, bottom: 12 })
      }
      .width('100%')
      .height(180)
      .borderRadius(8)
      .clip(true)

      // 价格区域
      Row() {
        Text(`¥${this.price.toFixed(2)}`)
          .fontSize(16)
          .fontColor('#FF5722')
          .fontWeight(FontWeight.Bold)

        if (this.originalPrice > 0) {
          Text(`¥${this.originalPrice.toFixed(2)}`)
            .fontSize(12)
            .fontColor('#9E9E9E')
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 8 })
        }

        Blank()

        Button('购买')
          .fontSize(12)
          .height(28)
          .backgroundColor('#007DFF')
          .borderRadius(14)
      }
      .width('100%')
      .padding(12)
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .shadow({ radius: 4, color: '#1A000000', offsetX: 0, offsetY: 2 })
  }
}

6.3 案例三:带 Loading 状态的按钮

需求描述

实现一个可复用的 Loading 按钮组件,点击后显示加载动画。

完整代码
@Component
struct LoadingButton {
  @Prop buttonText: string = '确定'
  @Prop isLoading: boolean = false
  @Prop isEnabled: boolean = true
  @Prop buttonWidth: number = 200
  @Prop buttonHeight: number = 44
  @Prop backgroundColor: string = '#007DFF'
  @Prop loadingText: string = '加载中...'

  onButtonClick: () => void = () => {}

  build() {
    Stack({ alignContent: Alignment.Center }) {
      // 底层:按钮主体
      Button(this.isLoading ? this.loadingText : this.buttonText)
        .width(this.buttonWidth)
        .height(this.buttonHeight)
        .backgroundColor(this.isEnabled ? this.backgroundColor : '#BDBDBD')
        .fontColor('#FFFFFF')
        .fontSize(14)
        .borderRadius(this.buttonHeight / 2)
        .enabled(this.isEnabled && !this.isLoading)
        .onClick(() => {
          if (!this.isLoading && this.isEnabled) {
            this.onButtonClick()
          }
        })

      // 顶层:Loading 遮罩
      if (this.isLoading) {
        Row({ space: 8 }) {
          LoadingProgress()
            .width(20)
            .height(20)
            .color('#FFFFFF')
          Text(this.loadingText)
            .fontSize(14)
            .fontColor('#FFFFFF')
        }
        .width(this.buttonWidth)
        .height(this.buttonHeight)
        .backgroundColor('#80000000')
        .borderRadius(this.buttonHeight / 2)
        .justifyContent(FlexAlign.Center)
      }
    }
    .width(this.buttonWidth)
    .height(this.buttonHeight)
  }
}

6.4 案例四:自定义进度条

需求描述

实现一个三层叠的进度条:背景、渐变填充、百分比文字。

完整代码
@Component
struct CustomProgressBar {
  @Prop progress: number = 0
  @Prop barHeight: number = 20
  @Prop showPercentage: boolean = true
  @Prop backgroundColor: string = '#E0E0E0'
  @Prop gradientStartColor: string = '#4CAF50'
  @Prop gradientEndColor: string = '#8BC34A'

  build() {
    Stack({ alignContent: Alignment.Start }) {
      // 底层:进度条背景
      Row()
        .width('100%')
        .height(this.barHeight)
        .backgroundColor(this.backgroundColor)
        .borderRadius(this.barHeight / 2)

      // 中层:进度填充
      Row()
        .width(`${Math.min(this.progress, 100)}%`)
        .height(this.barHeight)
        .linearGradient({
          direction: GradientDirection.Right,
          colors: [this.gradientStartColor, this.gradientEndColor]
        })
        .borderRadius(this.barHeight / 2)

      // 顶层:百分比文字
      if (this.showPercentage) {
        Row() {
          Text(`${Math.round(this.progress)}%`)
            .fontSize(this.barHeight > 24 ? 14 : 12)
            .fontColor('#333333')
            .fontWeight(FontWeight.Medium)
        }
        .width('100%')
        .height(this.barHeight)
        .justifyContent(FlexAlign.Center)
      }
    }
    .width('100%')
    .height(this.barHeight)
  }
}

6.5 案例五:带浮层的卡片布局

需求描述

实现一个卡片,左下角有悬浮的"更多"按钮,右上角有角标。

完整代码
@Component
struct FloatingCard {
  @Prop title: string
  @Prop description: string
  @Prop cornerBadge: string = ''

  onMoreClick: () => void = () => {}

  build() {
    Stack() {
      // 底层:卡片内容
      Column() {
        Text(this.title)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .margin({ bottom: 8 })

        Text(this.description)
          .fontSize(14)
          .fontColor('#666666')
          .maxLines(3)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16, bottom: 56 })

      // 右上角角标
      if (this.cornerBadge) {
        Text(this.cornerBadge)
          .fontSize(10)
          .fontColor('#FFFFFF')
          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
          .backgroundColor('#F44336')
          .borderRadius(4)
          .position({ x: '100%', y: 12 })
          .margin({ right: 16 })
      }

      // 左下角悬浮按钮
      Button() {
        Row({ space: 4 }) {
          Text('更多')
            .fontSize(12)
            .fontColor('#007DFF')
          Text('→')
            .fontSize(12)
            .fontColor('#007DFF')
        }
      }
      .height(32)
      .padding({ left: 12, right: 12 })
      .backgroundColor('#FFFFFF')
      .borderWidth(1)
      .borderColor('#007DFF')
      .borderRadius(16)
      .position({ x: 16, y: '100%' })
      .margin({ bottom: 12 })
      .onClick(() => {
        this.onMoreClick()
      })
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({ radius: 4, color: '#1A000000', offsetX: 0, offsetY: 2 })
  }
}

第七章:常见陷阱与最佳实践

7.1 Stack 的性能考量

Stack 本身不会导致性能问题,但不当使用可能引发问题。

7.1.1 控制层数

建议将 Stack 的层数控制在 3-5 层以内。对于不需要同时显示的内容,使用 if 条件渲染。

// ✅ 好的实践
Stack() {
  this.BackgroundLayer()
  if (this.showModal) {
    this.ModalLayer()
  }
}

// ❌ 避免
Stack() {
  Layer1()
  Layer2()
  Layer3()
  Layer4()
  Layer5()
  Layer6() // 过多
  Layer7()
}
7.1.2 合理使用 clip

对于超出 Stack 边界的内容,使用 clip(true) 裁剪,避免不必要的渲染开销。

7.2 点击穿透问题

Stack 中上层组件会拦截下层的点击事件。如果上层组件不需要处理点击:

Stack() {
  Column()
    .onClick(() => {
      console.log('点击了下层')
    })
  
  Column()
    .width('100%')
    .height('100%')
    .backgroundColor('#80000000')
    .enabled(false) // 禁用交互,让事件穿透
}

7.3 避免过度使用 Stack

Stack 并不是万能的。以下场景不适合使用 Stack:

  • 需要线性排列的内容(用 Column/Row)
  • 需要弹性布局的内容(用 Flex + layoutWeight)
  • 需要滚动的列表(用 List)

7.4 混合使用 Stack 和 Column/Row

实际开发中,Stack 和 Column/Row 经常需要组合使用:

Column() {
  // 卡片标题
  Text('商品列表')
  
  // Stack 实现的浮动标签卡片
  Stack() {
    Column() {
      // 卡片内容
    }
    
    // 悬浮标签
    Text('NEW')
      .position({ x: 12, y: 12 })
  }
}

7.5 调试技巧

开发时的调试技巧:

  • 给每层添加不同的背景色
  • 使用 DevEco Studio 的布局检查工具
  • 先布局再添加样式

第八章:API 24 新特性

8.1 FolderStack - 折叠屏专用 Stack

FolderStack 是 Stack 的派生组件,专为折叠屏设备设计。它支持在折叠状态下将部分子组件上移到上半屏,避开折叠区域。

FolderStack({ upperItems: ['topContent'] }) {
  Column()
    .id('topContent')
    .width('100%')
    .height(200)
    .backgroundColor('#E3F2FD')
  
  Column()
    .width('100%')
    .height(300)
    .backgroundColor('#BBDEFB')
}
.width('100%')
.height('500')

8.2 动态布局切换

结合状态管理,Stack 可以实现动态布局切换效果。

@State showFront: boolean = true

Stack() {
  // 背面内容
  Column()
    .opacity(this.showFront ? 0 : 1)
  
  // 正面内容
  Column()
    .opacity(this.showFront ? 1 : 0)
}
.onClick(() => {
  this.showFront = !this.showFront
})

8.3 与 Transition 动画结合

API 24 增强了 Stack 与 Transition 动画的结合,让层叠元素的进出场更流畅。

Stack() {
  if (this.showItem) {
    Column()
      .transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
  }
}

8.4 zIndex 的增强支持

API 24 对 zIndex 进行了优化,支持更精细的层级控制。


第九章:总结与选型决策树

9.1 核心原则

选择布局容器的核心原则:

  1. 需要层叠效果 → Stack
  2. 需要线性排列 → Column/Row
  3. 需要弹性分配 → Flex
  4. 需要网格布局 → Grid
  5. 需要滚动列表 → List

9.2 选型决策树

你的组件需要重叠吗?
  ├─ 是 → 使用 Stack
  │     ├─ 需要绝对定位?→ position()
  │     ├─ 需要相对偏移?→ offset()
  │     └─ 需要默认对齐?→ alignContent + margin
  │
  └─ 否 → 需要线性排列吗?
        ├─ 垂直排列 → Column
        │     ├─ 需要弹性布局?→ layoutWeight
        │     └─ 需要对齐控制?→ justifyContent / alignItems
        │
        └─ 水平排列 → Row
              ├─ 需要弹性布局?→ layoutWeight
              └─ 需要对齐控制?→ justifyContent / alignItems

9.3 常见误区

误区 正确做法
Stack 可以替代所有布局 Stack 只适合层叠场景
Column/Row 可以用 margin 实现层叠 层叠必须用 Stack
Stack 的层数越多越好 控制在 3-5 层以内
position 可以用于 Column/Row position 只适用于 Stack

9.4 最佳实践清单

  • 明确区分"排列"和"层叠"两种布局需求
  • 为每个组件选择最合适的容器
  • 控制 Stack 的层数在合理范围
  • 使用 position 进行精确层定位
  • 组合使用 Stack 和 Column/Row
  • 充分利用 zIndex 管理层级
  • 注意点击穿透问题
  • 响应式设计使用百分比

结语

布局容器是 ArkUI 的基石,正确选用 Column、Row 和 Stack 是每个 HarmonyOS 开发者的必修课。本文从原理到实战,系统地讲解了三种容器的特点、差异和使用场景。

记住这个核心判断:需要层叠 → Stack,需要排列 → Column/Row。当你遇到"在 A 上面放 B"的需求时,就该想到 Stack。

希望本文能帮助你在 HarmonyOS 开发的道路上更进一步!

Logo

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

更多推荐