鸿蒙原生 ArkTS 布局实战:Stack + LoadingProgress 实现全屏加载动画覆盖


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

一、引言

在移动应用开发中,「加载等待」是一个无法回避的场景——网络请求、数据解析、页面初始化等操作都需要时间。一个设计良好的加载动画不仅能缓解用户的焦虑情绪,还能提升应用的整体品质感。

HarmonyOS NEXT 作为华为全场景智慧生态的操作系统底座,提供了强大的 ArkUI 声明式 UI 框架。其中,Stack 容器LoadingProgress 组件的组合,是实现全屏加载动画覆盖最优雅、最原生的方案。

本文将以一个完整的示例项目为主线,深入讲解如何利用 Stack 容器实现「底层内容 + 顶层加载遮罩」的层叠布局,并结合 @State 响应式数据驱动 UI 显隐切换,打造流畅的加载体验。


二、核心概念速览

2.1 Stack 层叠容器

Stack 是 ArkUI 提供的层叠布局容器,其核心特征如下:

特性 说明
层叠规则 子组件按书写顺序自下而上堆叠,后写的子组件在上层
对齐方式 默认所有子组件从左上角开始定位,可通过 alignContent 修改
尺寸策略 默认自适应内容,可通过 .width() / .height() 显式设定
适用场景 遮罩层、悬浮按钮、徽章、加载覆盖、弹窗等

在 ArkTS 中,Stack 的层叠逻辑可以形象地理解为"后入先出"(Last In, First Rendered on Top),即越晚声明的子组件,在视觉上越靠前。

2.2 LoadingProgress 加载指示器

LoadingProgress 是 HarmonyOS 内置的加载动画组件,无需引入任何第三方库即可使用:

属性 类型 说明
.color(value) ResourceColor 设置加载圈颜色
.width(value) Length 设置加载圈宽度(直径)
.height(value) Length 设置加载圈高度(直径)

该组件会自动播放旋转动画,开发者只需控制其显示时机即可。

2.3 @State 响应式状态

@State 是 ArkTS 中最基础的状态装饰器:

  • @State 修饰的变量发生变化时,UI 自动重新渲染
  • 适用于组件内部的私有状态
  • 配合条件渲染(if / else),可实现加载层的显隐切换

三、完整代码实现

3.1 项目结构

entry/src/main/ets/pages/
└── Index.ets          ← 主页面,包含全部实现

3.2 完整代码

/*
 * 全屏加载动画覆盖(Stack + LoadingProgress)
 *
 * █ 布局要点:
 *  1. Stack 容器作为根布局,子组件按「后进先绘制」规则层叠。
 *  2. 底层(第一个子组件):主页面内容(如列表、卡片等)。
 *  3. 顶层(第二个子组件):全屏半透明遮罩 + LoadingProgress 加载动画。
 *  4. 通过 @State isLoading 控制加载层的显示/隐藏,实现自然的加载→内容过渡。
 *  5. LoadingProgress 配合 .width()/.height() 可调节加载圈大小。
 */

// ============ 必要的 import 语句 ============
// Stack、LoadingProgress、Text、Column、Row、Divider、Blank、ForEach 等均为
// 鸿蒙 ArkUI 框架内置组件,无需额外 import。
// 如需使用 router 能力可在此引入:
// import { router } from '@kit.ArkUI';

// ============ 页面入口 ============
@Entry
@Component
struct Index {

  /* ---------- 状态变量 ---------- */
  @State isLoading: boolean = true;   // 是否处于加载状态(控制加载层显隐)
  @State progressValue: number = 0;   // 模拟进度值(0~100)
  private progressTimer: number = -1; // 定时器 ID

  /* ---------- 生命周期:页面加载完成时自动触发加载动画 ---------- */
  aboutToAppear(): void {
    // 启动模拟加载任务:每 30ms 增加一次进度
    this.progressTimer = setInterval(() => {
      if (this.progressValue < 100) {
        this.progressValue += 2;
      } else {
        // 加载完毕 → 关闭加载层,清除定时器
        this.isLoading = false;
        clearInterval(this.progressTimer);
        this.progressTimer = -1;
      }
    }, 30);
  }

  /* ---------- 页面卸载时清理定时器 ---------- */
  aboutToDisappear(): void {
    if (this.progressTimer !== -1) {
      clearInterval(this.progressTimer);
      this.progressTimer = -1;
    }
  }

  /* ---------- UI 构建 ---------- */
  build() {
    // ─────────────────────────────────────────────
    // 核心:Stack 容器实现层叠效果
    //  ┌─────────────────────┐
    //  │   主页面内容(底层)│  ← 始终存在,被加载层遮挡时不可见
    //  ├─────────────────────┤
    //  │  加载遮罩层(顶层) │  ← isLoading=true 时显示,false 时消失
    //  └─────────────────────┘
    // ─────────────────────────────────────────────
    Stack() {
      /* ======== 1. 底层:主页面内容 ======== */
      Column() {
        // 标题区
        Text('鸿蒙原生 ArkTS 布局示例')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1A73E8')
          .margin({ top: 20, bottom: 8 });

        Text('Stack + LoadingProgress 全屏加载覆盖')
          .fontSize(14)
          .fontColor('#666666')
          .margin({ bottom: 20 });

        // 分隔线
        Divider()
          .strokeWidth(1)
          .color('#E0E0E0')
          .width('90%')
          .margin({ bottom: 16 });

        // 模拟内容卡片区
        ForEach(this.getMockCards(), (item: MockCard, index: number) => {
          CardItem({ title: item.title, desc: item.desc, color: item.color })
        }, (item: MockCard, index: number) => item.title + index.toString());

        // 底部说明
        Blank()
          .layoutWeight(1);

        Text('加载完成后自动进入主页面')
          .fontSize(12)
          .fontColor('#AAAAAA')
          .margin({ bottom: 30 });
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#F5F5F5')
      .padding({ left: 16, right: 16 })
      // ⚠️ 关键:底层必须占满 Stack,否则加载层无法全屏覆盖
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]);

      /* ======== 2. 顶层:全屏加载遮罩层 ======== */
      // 条件渲染:isLoading=true 时显示加载层
      if (this.isLoading) {
        Column() {
          // ── 加载动画核心 ──
          LoadingProgress()
            .width(48)           // 加载圈直径 48vp
            .height(48)
            .color('#1A73E8')    // 蓝色加载圈,与标题色呼应
            .margin({ bottom: 20 });

          // 加载提示文字
          Text('正在加载...')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Medium)
            .margin({ bottom: 12 });

          // 进度百分比显示
          Text(this.progressValue + '%')
            .fontSize(13)
            .fontColor('rgba(255,255,255,0.8)');
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)  // 主轴居中
        .alignItems(HorizontalAlign.Center) // 交叉轴居中
        // ⚠️ 关键:半透明遮罩背景,覆盖全屏
        .backgroundColor('rgba(0, 0, 0, 0.6)')
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]);
      }
    }
    .width('100%')
    .height('100%')
    // ⚠️ 关键:Stack 默认子组件从左上角开始排列,
    // 每个子组件都设为 100% 宽高即可实现全屏覆盖
  }

  /* ---------- 模拟数据 ---------- */
  getMockCards(): MockCard[] {
    return [
      { title: '布局方式:Stack',       desc: '层叠容器,子组件按顺序堆叠',             color: '#E3F2FD' },
      { title: '组件:LoadingProgress', desc: '系统加载动画指示器,支持颜色/大小定制',  color: '#E8F5E9' },
      { title: '状态控制:@State',      desc: '响应式数据驱动 UI 显隐切换',              color: '#FFF3E0' },
      { title: '动画效果:渐显过渡',    desc: '加载完成后遮罩平滑消失',                  color: '#F3E5F5' },
    ];
  }
}

/* ============ 自定义子组件:卡片 ============ */
@Component
struct CardItem {
  @Prop title: string = '';
  @Prop desc: string = '';
  @Prop color: string = '#FFFFFF';

  build() {
    Row() {
      // 左侧色块
      Column()
        .width(6)
        .height('100%')
        .backgroundColor(this.color)
        .borderRadius({ topLeft: 8, bottomLeft: 8 });

      // 右侧文字
      Column() {
        Text(this.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')
          .width('100%');

        Text(this.desc)
          .fontSize(13)
          .fontColor('#888888')
          .width('100%')
          .margin({ top: 4 });
      }
      .alignItems(HorizontalAlign.Start)
      .padding({ left: 12, right: 12, top: 12, bottom: 12 });
    }
    .width('100%')
    .height(72)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .margin({ bottom: 10 })
    // ⚠️ 注意:鸿蒙 ArkTS 的阴影 API 在 API 12+ 中为 .shadow()
    .shadow({
      radius: 4,
      color: 'rgba(0,0,0,0.08)',
      offsetX: 0,
      offsetY: 2
    })
    .alignItems(VerticalAlign.Center);
  }
}

/* ============ 数据模型 ============ */
interface MockCard {
  title: string;
  desc: string;
  color: string;
}

四、布局原理解析

4.1 层叠结构拆解

本示例的核心布局仅由 一个 Stack 容器 + 两个子组件 构成:

Stack(根容器,宽高 100%)
├── Column(底层:主页面内容)      宽高 100%
│   ├── Text(标题)
│   ├── Divider(分隔线)
│   ├── ForEach → CardItem × 4(卡片列表)
│   └── Text(底部提示)
│
└── Column(顶层:加载遮罩层)      宽高 100%,条件渲染
    ├── LoadingProgress(加载动画圈)
    ├── Text("正在加载...")
    └── Text(进度百分比)

关键要点

  1. 两个子组件均设置 width('100%')height('100%')——这是实现全屏覆盖的前提条件。如果底层 Column 没有撑满 Stack,顶层遮罩也无法覆盖到边缘区域。

  2. 顶层使用 if (this.isLoading) 条件渲染——当 isLoadingtrue 时,遮罩层存在并覆盖在内容之上;当加载完成、isLoading 变为 false 时,遮罩层从组件树中移除,底层内容自然暴露。

  3. expandSafeArea 扩展安全区域——通过 expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) 让遮罩层延伸到状态栏和导航栏区域,实现真正的全屏覆盖。

4.2 数据驱动显隐切换

ArkTS 的状态管理机制是本示例能够流畅运行的根本原因:

用户操作 / 网络请求 / 定时器
        ↓
  修改 @State 变量
        ↓
  ArkUI 框架自动检测状态变化
        ↓
  触发组件树的局部更新
        ↓
  加载层移入/移出组件树
        ↓
  底层内容显示/隐藏

整个流程中,开发者只需要关注业务逻辑(修改状态),UI 同步由框架自动完成

4.3 生命周期管理

aboutToAppear() 中启动定时器模拟加载,在 aboutToDisappear() 中清理定时器——这是 ArkTS 标准的生命周期管理方式:

生命周期方法 调用时机 作用
aboutToAppear() 组件创建后、build() 前 初始化数据、启动异步任务
aboutToDisappear() 组件销毁前 清理定时器、取消订阅、释放资源

五、常见问题与最佳实践

5.1 为什么选择 Stack 而非 Column / Flex?

  • Column / Flex 是线性布局,子组件沿主轴排列,无法实现层叠覆盖效果。
  • Stack 允许子组件在 Z 轴方向上堆叠,天然适合遮罩、悬浮层等场景。
  • 如果加载层不需要覆盖全屏(如仅覆盖内容区域、保留顶部导航栏),可将 Stack 嵌套在页面内而非作为根容器。

5.2 如何实现加载层的过渡动画?

本示例中加载层的消失是瞬间移除的。如需淡入淡出效果,可使用 ArkUI 的 animateTo 或显式动画:

// 淡出效果示例(在 isLoading 变为 false 前调用)
animateTo({ duration: 300, curve: Curve.EaseInOut }, () => {
  this.isLoading = false;
});

5.3 LoadingProgress 的局限性

  • LoadingProgress 仅提供无限旋转的加载动画,不支持进度条模式。
  • 如果需要圆形进度条(可显示百分比),应使用 Progress 组件并设置 type: ProgressType.Ring
  • 可以通过 .color() 修改颜色以适配品牌色调。

5.4 性能注意事项

  • 条件渲染 vs 透明度控制:使用 if 条件渲染(本示例)会在隐藏时完全移除节点,内存更优;使用 .opacity(0) 配合 .hitTestBehavior(HitTestBehavior.None) 则保留节点但不可交互,适合需要频繁显隐的场合。
  • 定时器清理:务必在 aboutToDisappear() 中清理定时器,防止页面销毁后定时器仍在执行导致内存泄漏或崩溃。
  • 避免过度嵌套:Stack 内部不宜嵌套过多层 Stack,会影响布局性能和可维护性。

六、效果对比:有加载动画 vs 无加载动画

场景 无加载动画 有加载动画(本方案)
用户体验 页面白屏或卡顿,用户可能误以为闪退 视觉连续,用户感知到"正在加载"
感知等待时间 感觉漫长(即使实际耗时很短) 感觉流畅(加载动画分散了注意力)
品牌感知 粗糙、未完成 精致、专业
实现成本 0 极低(约 50 行代码)

七、扩展思路

7.1 结合网络请求

isLoading 的赋值与实际网络请求绑定:

async loadData() {
  this.isLoading = true;
  try {
    const data = await HttpUtil.get('/api/data');
    // 处理数据...
  } finally {
    this.isLoading = false;
  }
}

7.2 多阶段加载

使用不同的加载状态展示不同的遮罩内容:

enum LoadState { LOADING, SUCCESS, ERROR }

@State loadState: LoadState = LoadState.LOADING;

// 在 if 中根据 loadState 显示加载中/错误重试/内容

7.3 配合骨架屏

在加载层中展示与内容结构相似的骨架屏占位符(Skeleton),让用户提前感知页面结构,进一步降低等待焦虑。


八、结语

本文通过一个完整的全屏加载动画覆盖示例,展示了 HarmonyOS NEXT 原生 ArkTS 布局中 Stack 层叠容器LoadingProgress 加载指示器 的核心用法。

总结要点:

  1. Stack 容器是实现层叠布局的核心工具,子组件按书写顺序自下而上堆叠。
  2. LoadingProgress 提供系统级的加载动画效果,零依赖、高性能。
  3. @State 状态管理驱动 UI 条件渲染,实现加载层的精准显隐。
  4. 生命周期管理确保定时器等资源的正确释放,防止内存泄漏。
  5. 全屏覆盖需要子组件设置 100% 宽高并调用 expandSafeArea

这套布局方案代码量少、可读性强、性能优异,是鸿蒙原生开发中实现加载等待效果的推荐方案。无论是简单的数据加载,还是复杂的多阶段初始化流程,都可以在此基础上灵活扩展。


本文配套示例代码已托管在项目中,欢迎运行体验。如有疑问或建议,欢迎留言讨论。


参考资料

Logo

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

更多推荐