在这里插入图片描述

📖 引言

打开任何一个 App,你看到的第一页是什么?是启动页(Splash Page,也叫闪屏页)。

别小看这短短 2 秒的页面,它承担着重要的使命:

  • 品牌展示:让用户第一眼就记住你的品牌
  • 加载缓冲:应用初始化需要时间,启动页填补这段空白
  • 用户预期:告诉用户"应用正在启动,马上就好"
  • 氛围营造:奠定整个应用的设计风格和调性

一个好的启动页,应该是"快而不闪,动而不炫"——

  • 太快了,用户还没看清就跳走了,品牌没记住
  • 太慢了,用户等得不耐烦,直接关掉
  • 动画太花哨,喧宾夺主,反而显得不专业

「民族图鉴」的启动页是怎么设计的呢?——用一个圆形的"56"图腾 Logo,配合四阶段动画序列:Logo 缩放淡入 → 标题淡入 → 副标题淡入 → 底部进度条。整个过程约 2 秒,节奏舒缓,有层次感。

这一篇,我们就从启动页开始,深入讲解鸿蒙的动画系统。你将学会:属性动画怎么用?动画曲线怎么选?多阶段动画序列怎么编排?启动页的最佳实践是什么?

让我们从第一页开始,构建一个专业的鸿蒙应用。


🎯 学习目标

完成本文后,你将能够:

  • ✅ 理解启动页的设计原则与作用
  • ✅ 掌握属性动画的工作原理与使用方法
  • ✅ 学会用 setTimeout 编排多阶段动画序列
  • ✅ 理解不同动画曲线(EaseIn/EaseOut/Linear)的适用场景
  • ✅ 掌握 scale/opacity/width 等常用动画属性
  • ✅ 了解 animateTo 显式动画与属性动画的区别
  • ✅ 写出有层次感、节奏感的启动页动画

🎨 启动页的设计原则

启动页虽短,但它是用户与应用的第一次接触,其设计直接影响用户对应用的第一印象。好的启动页设计需要遵循以下核心原则:

原则一:品牌展示——建立第一印象

启动页是品牌曝光的第一触点,用户在等待的 1-2 秒内,应该能清晰地记住"这是谁的应用"。

品牌展示的三要素

要素 说明 「民族图鉴」实践
Logo 品牌标识,最核心的识别元素 圆形"56"图腾 Logo,主色调填充
应用名称 告诉用户这是什么应用 "民族图鉴"四个大字,加粗醒目
Slogan 一句话说明应用价值 “56个民族,一部史诗”,传递文化厚重感

设计要点

  • Logo 要清晰可识别,不要放得太小或太偏
  • 应用名称要用易读的字体,不要用过于艺术化的字体
  • Slogan 要简短有力,不超过 12 个字
  • 整体视觉风格要和应用主界面保持一致

💡 为什么「民族图鉴」用圆形 Logo? 圆形在视觉心理学上给人完整、和谐、温暖的感觉,与"民族团结"的主题相呼应。同时,圆形 Logo 在方形手机屏幕上更突出,更容易被用户记住。

原则二:减少等待感——让等待变得可感知

用户讨厌等待,但应用初始化确实需要时间。启动页的使命之一,就是让等待变得不那么难熬

减少等待感的心理学技巧

1. 进度指示器

  • 告诉用户"正在加载,还有多久",比干等着让人舒服得多
  • 即使不知道准确进度,一个不确定的加载动画也比静态页面好
  • 「民族图鉴」用底部进度条,既是装饰,又是进度指示

2. 有趣的动画

  • 用户的注意力被动画吸引,就会觉得时间过得更快
  • 但动画不能太花哨,否则会喧宾夺主
  • 「民族图鉴」的四阶段动画,节奏舒缓,有层次感,用户看着不觉得无聊

3. 控制时长

  • 1.5-2.5 秒是黄金区间
  • 太短:用户还没看清就跳走了,品牌没记住
  • 太长:用户等得不耐烦,可能直接关掉
  • 「民族图鉴」控制在 2.2 秒左右,不长不短

💡 等待心理学:研究表明,用户对等待时间的感知,和实际等待时间并不完全一致。有进度提示的等待,用户感觉比实际时间短 30%;有动画的等待,感觉比实际时间短 20%。这就是为什么启动页一定要有动画和进度指示。

原则三:快速进入——不要让用户等太久

启动页只是过渡,不是目的。用户打开应用是为了用功能,不是为了看启动页。

快速进入的设计要点

策略 说明 怎么做
最短展示时间 避免闪一下就没了 设置 1 秒最短展示时间
最长等待时间 避免一直等下去 设置 3 秒超时,不管加载完没完成都跳
数据预加载 启动页期间同时加载数据 动画和数据加载并行
跳过按钮 给用户选择权 加个"跳过"按钮,用户不想看可以跳过

「民族图鉴」的策略

  • 动画时长 2 秒 + 200ms 缓冲 = 2.2 秒总时长
  • 初始化工作放在首页,启动页只负责展示品牌
  • 没有跳过按钮——2.2 秒很短,加跳过按钮反而增加视觉负担

💡 什么时候需要跳过按钮? 如果启动页时长超过 3 秒,或者启动页上有广告,就必须加跳过按钮。如果只是品牌展示,2 秒左右,不加也没关系。加不加跳过按钮,本质上是"品牌曝光"和"用户体验"的权衡。

原则四:氛围营造——奠定应用调性

启动页是应用的"门面",它的设计风格、色彩、动效,会给用户留下"这个应用是什么调性"的第一印象。

不同应用的启动页调性对比

应用类型 调性 启动页特点 动画风格
工具类 高效、简洁 纯色背景 + 简洁 Logo 快速、利落
游戏类 刺激、好玩 游戏画面 + 角色 炫酷、动感
社交类 温暖、友好 人物插画 + 标语 柔和、亲切
文化类 典雅、厚重 传统元素 + 文化符号 舒缓、优雅
电商类 热闹、丰富 促销信息 + 商品 快速、多样

「民族图鉴」的调性定位

  • 文化类应用,走典雅、厚重、有质感的路线
  • 主色调:中国红 + 金色,传递传统文化的厚重感
  • 动画风格:舒缓、优雅、有层次感,不疾不徐
  • 视觉元素:圆形图腾、民族文字、传统纹样

启动页的调性,要和应用的整体设计语言保持一致。如果启动页是活泼的卡通风格,进去之后却是严肃的商务风格,用户会有强烈的违和感。一致性,是设计的基本原则。


💡 需求分析

启动页的核心需求

启动页虽然简单,但需求不少:

需求点 说明 为什么重要
品牌展示 Logo + 应用名称 + Slogan 用户建立第一印象的地方
动画效果 不能是静态的,要有动效 显得应用有活力、有品质
时长控制 1.5-2.5 秒之间 太短没存在感,太长用户烦
自动跳转 动画结束自动跳首页 不需要用户操作,流畅过渡
异常处理 跳转失败要有兜底 不能卡在启动页动不了
性能 动画流畅,不卡顿 第一印象很重要,卡了就完了

「民族图鉴」启动页设计方案

我们的启动页走的是简约民族风路线:

┌─────────────────────────┐
│                         │
│                         │
│      ┌─────────┐        │
│      │         │        │
│      │   56    │        │  ← 圆形Logo(主色调填充,白字"56")
│      │ 民族    │        │
│      │         │        │
│      └─────────┘        │
│                         │
│                         │
│      民 族 图 鉴        │  ← 应用名称(大标题)
│                         │
│    56个民族,一部史诗    │  ← 副标题 / Slogan
│                         │
│        ─────            │  ← 装饰线 / 进度条
│                         │
│                         │
└─────────────────────────┘

动画序列设计(四阶段,总时长约 2 秒):

阶段 时间 内容 效果
第一阶段 0 - 800ms Logo 缩放淡入 从 0.5 倍放大到 1 倍,同时透明度从 0 到 1
第二阶段 600 - 1200ms 标题淡入 应用名称渐显,从透明到完全显示
第三阶段 1000 - 1600ms 副标题淡入 Slogan 渐显,和装饰线一起出现
第四阶段 1200 - 2000ms 进度条延伸 底部装饰线从 0 延伸到 80vp
跳转 2200ms 跳转到首页 动画结束后 200ms,跳转首页

注意看时间,动画之间有重叠——Logo 还在放大呢,标题已经开始淡入了。这样节奏更紧凑,不会拖沓。

更多启动页样式参考

「民族图鉴」选择的是"简约 Logo + 文字"风格,但启动页的样式远不止这一种。不同的应用,适合不同的启动页风格。

样式一:全屏图片式

用一张高质量的全屏图片作为启动页背景,视觉冲击力最强。

┌─────────────────────────┐
│                         │
│                         │
│    [全屏背景图片]       │
│                         │
│                         │
│                         │
│                         │
│        品牌 Logo        │
│                         │
│      应用名称 + 副标题  │
│                         │
│      [加载指示器]       │
│                         │
└─────────────────────────┘

适用场景

  • 品牌形象强、视觉冲击力要求高的应用
  • 旅游、摄影、设计类应用
  • 有精美插画资源的应用

优缺点

优点 缺点
视觉冲击力强,品牌记忆深刻 图片加载需要时间,可能增加白屏时间
氛围营造效果好 图片适配不同屏幕尺寸麻烦
用户体验好 包体积增大(高清图片占空间)

💡 「民族图鉴」为什么不用全屏图片? 虽然民族题材有很多精美图片,但我们选择了更克制的设计。原因有三:一是全屏图片加载慢,影响启动速度;二是文化类应用应该更注重内容而非形式;三是简约风格更耐看,用户不容易审美疲劳。

样式二:渐变背景式

用渐变色作为背景,简洁又有设计感。

┌─────────────────────────┐
│                         │
│     [渐变背景]          │
│                         │
│                         │
│        ░░░░░            │
│        ░Logo░           │
│        ░░░░░            │
│                         │
│      应用名称           │
│                         │
│      ————————           │
│                         │
│      加载中...          │
│                         │
└─────────────────────────┘

适用场景

  • 工具类、效率类应用
  • 品牌色鲜明的应用
  • 追求简约设计风格的应用

实现代码示例

// 渐变背景启动页
@Entry
@Component
struct GradientSplash {
  @State opacity: number = 0;

  aboutToAppear(): void {
    animateTo({ duration: 800, curve: Curve.EaseOut }, () => {
      this.opacity = 1;
    });
  }

  build() {
    Column() {
      // Logo
      Text('56')
        .fontSize(64)
        .fontColor(Color.White)
        .fontWeight(FontWeight.Bold)
      
      Text('民族图鉴')
        .fontSize(32)
        .fontColor(Color.White)
        .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .linearGradient({
      colors: [['#FF6B6B', 0.0], ['#C44569', 1.0]],
      direction: GradientDirection.Bottom
    })
    .opacity(this.opacity)
  }
}
样式三:加载指示器式

在启动页底部加上明确的加载指示器,告诉用户"应用正在加载,请稍候"。

常见的加载指示器类型

类型 说明 适用场景
进度条 显示准确进度 加载进度可预估的场景
旋转圆圈 不确定进度的加载 网络请求、数据初始化
跳动的点 三个点依次跳动 现代感强的应用
脉冲动画 圆形脉冲扩散 品牌感强的应用
骨架屏 页面结构占位 内容型应用

「民族图鉴」的选择:我们用了底部进度条 + 装饰线的设计,既是装饰,又暗示了加载进度。这种设计比单纯的加载圆圈更有设计感,和整体风格也更搭。


🔍 启动白屏/黑屏的原因分析与解决方案

很多开发者都会遇到这个问题:点击应用图标,先白屏(或黑屏)一下,然后才显示启动页。这是怎么回事?怎么解决?

白屏/黑屏的根本原因

白屏/黑屏的本质是:应用进程已经启动,但页面还没渲染出来

从用户点击图标到看到启动页,中间经历了这些阶段:

用户点击图标
    ↓
系统创建应用进程(冷启动)
    ↓
加载 Ability + 初始化应用上下文
    ↓
加载页面路由配置
    ↓
创建页面实例
    ↓
执行 aboutToAppear
    ↓
页面首次渲染(build 执行)
    ↓
用户看到启动页内容

在"页面首次渲染"之前,用户看到的就是白屏或黑屏——窗口已经创建了,但内容还没画上去。

白屏 vs 黑屏

现象 原因 什么时候出现
白屏 窗口背景是白色的,内容还没渲染 浅色主题应用
黑屏 窗口背景是黑色的,内容还没渲染 深色主题应用,或系统默认

其实说白屏或黑屏都不准确——准确地说,是"窗口背景色"。如果你的应用主题是浅色的,窗口背景默认是白色,所以叫白屏;如果是深色的,窗口背景默认是黑色,所以叫黑屏。

解决方案总览

解决白屏/黑屏问题,有几种常见思路:

方案 原理 难度 效果
Ability 主题设置 把窗口背景设成和启动页一样的颜色 ⭐⭐⭐
启动图(Splash) 系统层面显示一张图片,应用准备好后消失 ⭐⭐ ⭐⭐⭐⭐
优化启动速度 减少初始化时间,让页面快点渲染出来 ⭐⭐⭐ ⭐⭐⭐
延迟加载 把非必要的初始化推迟到首页 ⭐⭐ ⭐⭐⭐

下面我们逐一讲解。

方案一:Ability 主题设置(推荐)

最简单、成本最低的方案:把 Ability 的窗口背景色,设成和启动页背景色一样。这样即使页面还没渲染出来,用户看到的也是和启动页一样的颜色,感觉不到白屏。

实现方法

module.json5 或 Ability 的配置中,设置窗口背景色。

// module.json5 中的配置示例
{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "launchType": "standard",
        "backgroundModes": [],
        "startWindowIcon": "$media:app_icon",
        "startWindowBackground": "$color:page_bg",
      }
    ]
  }
}

关键配置项

  • startWindowBackground:启动窗口的背景色,设成和启动页背景一样
  • startWindowIcon:启动图标,可选

💡 为什么这招有效? 因为应用启动时,系统会先显示一个"启动窗口"(Start Window),这个窗口的背景色就是 startWindowBackground 配置的颜色。等应用页面渲染好之后,这个启动窗口会自动消失。如果启动窗口的颜色和启动页的颜色一样,用户就感觉不到切换,以为启动页一下子就出来了。

方案二:系统级启动图(Splash Screen)

Android 有 Splash Screen API,iOS 有 Launch Screen,鸿蒙也有类似的机制——在系统层面显示一张启动图,应用准备好之后自动消失。

优点

  • 体验最好,用户点击图标立即看到启动图
  • 系统层面实现,性能最好
  • 不会出现白屏/黑屏

缺点

  • 只能是静态图片,不能有动画
  • 定制化程度有限
  • 适配不同屏幕尺寸麻烦

适用场景

  • 对启动体验要求极高的应用
  • 品牌 Logo 简单、适合静态展示的应用

「民族图鉴」为什么没用这个方案?因为我们的启动页有四阶段动画,静态图片展示不了动画效果。而且,通过 Ability 主题设置 + 优化启动速度,已经能做到几乎感知不到白屏了。

方案三:优化启动速度

从根本上解决问题——让应用启动得更快,白屏时间自然就短了。

启动速度优化的几个方向

1. 减少 Application 级别的初始化

// ❌ 不要在 Ability 的 onCreate 里做太多事
onCreate(want, launchParam) {
  this.initDatabase();      // 慢!数据库初始化
  this.initNetwork();       // 慢!网络库初始化
  this.initAnalytics();     // 慢!统计 SDK 初始化
  this.initCrashReport();   // 慢!崩溃上报初始化
  // ... 一堆初始化
}

// ✅ 延迟到需要的时候再初始化
onCreate(want, launchParam) {
  // 只做最必要的初始化
  AppStorage.setOrCreate('appCreated', true);
}

// 在首页的 aboutToAppear 里再初始化其他的

2. 首屏页面尽量简单

  • 启动页的布局越简单,渲染越快
  • 尽量用基础组件,少用复杂的自定义组件
  • 图片尽量小,或者不用图片(「民族图鉴」用图形 + 文字,性能最好)

3. 资源预加载

  • 常用资源可以提前加载
  • 但不要在启动阶段加载太多,反而拖慢启动

启动速度优化是一个系统工程,不是一两句话能说清的。但对大多数应用来说,做到"启动页不搞复杂、Ability onCreate 不做重活",白屏时间就已经很短了,用户几乎感知不到。

方案四:「民族图鉴」的实践

「民族图鉴」是怎么做的呢?我们用了组合方案

  1. Ability 背景色和启动页一致:从根源上消除白屏感
  2. 启动页保持简洁:用 Text + Column 实现,不用图片,渲染快
  3. 初始化推迟到首页:启动页只放动画,不做任何重活
  4. 动画立即开始:aboutToAppear 里马上触发动画,不等待
// 启动页的 aboutToAppear,轻量到极致
aboutToAppear(): void {
  this.startAnimation();    // 只启动动画
  this.navigateToHome();    // 只开始计时
}

实际效果是:用户点击图标 → 瞬间看到和启动页同色的背景 → 几乎立即就看到 Logo 开始放大 → 完整动画序列 → 进入首页。整个过程流畅自然,完全感觉不到白屏。

💡 白屏一定需要解决吗? 不一定。如果你的应用启动很快(< 100ms),用户根本感知不到白屏,那就没必要专门处理。但如果启动慢,白屏时间超过 200ms,用户就能明显感觉到"卡了一下",这时候就需要优化了。


📊 启动页的三种实现方式对比

实现启动页,不止一种方式。鸿蒙生态中,常见的有三种实现方式:Ability 样式方式页面动画方式系统 Splash 组件方式。各有优劣,适合不同场景。

方式一:Ability 样式方式(系统级)

原理:通过配置 Ability 的主题、启动窗口背景、启动图标,让系统在应用启动时显示一个静态的启动画面。应用准备好之后,系统自动关闭启动窗口,显示应用内容。

实现方式

  • module.json5 中配置 startWindowBackgroundstartWindowIcon
  • 纯配置,不需要写代码
  • 系统层面实现
{
  "abilities": [
    {
      "name": "EntryAbility",
      "startWindowIcon": "$media:app_icon",
      "startWindowBackground": "$color:page_bg",
    }
  ]
}

优缺点

优点 缺点
启动最快,点击图标立即显示 只能是静态的,不能有动画
系统级实现,性能最好 定制化程度低,只能放图片和颜色
完全不会白屏/黑屏 无法控制展示时长
不需要写代码 无法做品牌故事、引导页等复杂内容

适用场景

  • 工具类应用,追求启动速度
  • 启动页只需要展示 Logo 的简单场景
  • 对启动体验要求极高的应用

方式二:页面动画方式(应用级)

原理:把启动页做成应用的第一个页面,在页面里用代码实现各种动画效果。动画结束后,再跳转到首页。

实现方式

  • 写一个独立的 SplashPage 页面
  • 在页面里用属性动画、显式动画实现动效
  • 用 setTimeout 控制时长和跳转
  • 这也是「民族图鉴」采用的方式
@Entry
@Component
struct SplashPage {
  @State logoScale: number = 0.5;
  @State logoOpacity: number = 0;
  // ... 更多状态

  aboutToAppear(): void {
    this.startAnimation();
    this.navigateToHome();
  }

  build() {
    Column() {
      // Logo + 标题 + 副标题 + 进度条
    }
  }
}

优缺点

优点 缺点
完全自定义,想做什么动画都行 会有短暂的白屏(需要优化)
可以实现复杂的品牌故事、引导页 需要写代码,工作量大
可以控制展示时长、跳转逻辑 启动速度比系统级慢一点
可以加广告、活动页等内容 动画不流畅会给用户"卡"的感觉

适用场景

  • 启动页需要动画效果的应用
  • 有品牌展示需求的应用
  • 启动页需要做引导、广告等功能的应用

方式三:系统 Splash 组件方式(组件级)

原理:使用鸿蒙系统提供的 Splash 组件(如果有的话),或者第三方封装的启动页组件,快速实现启动页。

说明

  • 鸿蒙官方目前没有统一的 Splash 组件(不像 Flutter 有官方 SplashScreen)
  • 社区有一些第三方封装的启动页组件
  • 本质上还是方式二的封装,只是帮你把常用逻辑封装好了

优缺点

优点 缺点
开箱即用,快速实现 依赖第三方库,可能有兼容性问题
常用功能都封装好了 定制化程度有限
最佳实践内置,不容易踩坑 可能有额外的包体积

适用场景

  • 快速原型开发
  • 对启动页要求不高的应用
  • 不想自己写启动页逻辑的场景

三种方式对比总结

对比维度 Ability 样式方式 页面动画方式 系统 Splash 组件
启动速度 ⭐⭐⭐⭐⭐ 最快 ⭐⭐⭐ 中等 ⭐⭐⭐ 中等
白屏问题 ⭐⭐⭐⭐⭐ 完全没有 ⭐⭐⭐ 需要优化 ⭐⭐⭐ 需要优化
动画效果 ⭐ 只有静态 ⭐⭐⭐⭐⭐ 完全自定义 ⭐⭐⭐ 有限
开发成本 ⭐ 最低(配置就行) ⭐⭐⭐ 中等 ⭐⭐ 较低
定制化程度 ⭐ 很低 ⭐⭐⭐⭐⭐ 最高 ⭐⭐⭐ 中等
包体积影响 ⭐⭐⭐⭐⭐ 无影响 ⭐⭐⭐⭐ 很小 ⭐⭐⭐ 有一点
推荐指数 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐

「民族图鉴」的选择理由

我们为什么选择了页面动画方式

  1. 动画需求:我们需要四阶段动画序列(Logo 缩放 → 标题淡入 → 副标题淡入 → 进度条),静态启动图实现不了
  2. 品牌调性:文化类应用需要有仪式感,优雅的动画能提升品牌质感
  3. 控制能力:可以精确控制每个元素的动画时机、时长、曲线
  4. 可扩展性:以后要加开屏广告、节日启动页、版本更新引导,都很方便
  5. 白屏可优化:通过 Ability 背景色设置 + 轻量启动页,白屏时间可以控制在用户感知不到的程度

没有最好的方式,只有最适合的方式。如果你的应用追求极致启动速度,选方式一;如果你的应用需要精美的启动动画,选方式二;如果你想快速搞定,选方式三。


⏱️ 启动时间的测量与优化

启动页做得好不好,不能只看视觉效果,还要看数据。启动时间多长?用户等了多久?这些都需要量化。

启动时间的定义

首先,我们要明确"启动时间"到底是从哪到哪。

冷启动的完整流程

用户点击图标
    ↓ 【点1】应用进程创建
系统创建应用进程
    ↓
加载 Ability
    ↓ 【点2】Ability 创建完成
Application/Ability 初始化
    ↓
加载路由配置
    ↓
创建启动页实例
    ↓ 【点3】启动页首次渲染完成
启动页动画播放
    ↓ 【点4】跳转到首页
跳转首页
    ↓ 【点5】首页内容渲染完成
用户可以操作

常见的启动时间指标

指标 定义 从哪到哪 重要性
首帧时间 用户看到第一帧画面的时间 点击 → 启动页渲染完成 ⭐⭐⭐⭐⭐
可交互时间 用户可以开始操作的时间 点击 → 首页渲染完成 ⭐⭐⭐⭐⭐
启动页展示时长 启动页展示了多久 启动页出现 → 跳走 ⭐⭐⭐
白屏时间 白屏持续了多久 点击 → 首帧 ⭐⭐⭐⭐

对用户来说,最关键的是首帧时间——从点击图标到看到第一帧内容。如果首帧时间超过 1 秒,用户就会觉得"这个应用有点慢";如果超过 2 秒,用户可能就失去耐心了。

怎么测量启动时间?

方法一:手动打日志

最简单的方法:在关键时间点打印日志,然后计算差值。

// EntryAbility.ets
onCreate(want, launchParam) {
  const startTime = Date.now();
  AppStorage.setOrCreate('appCreateTime', startTime);
  console.info('AppStart', 'Ability onCreate: ' + startTime);
}

// SplashPage.ets
aboutToAppear(): void {
  const createTime = AppStorage.get<number>('appCreateTime') || 0;
  const now = Date.now();
  console.info('AppStart', 'SplashPage aboutToAppear: ' + now);
  console.info('AppStart', '白屏耗时: ' + (now - createTime) + 'ms');
  
  this.startAnimation();
  this.navigateToHome();
}

build() {
  // 页面构建完成后打日志
  console.info('AppStart', 'SplashPage build executed');
}

看日志就能知道

  • 从 Ability 创建到启动页 aboutToAppear,花了多久(这就是白屏时间)
  • 动画总时长是多少
  • 从点击到跳转到首页,总共花了多久
方法二:用性能分析工具

鸿蒙有专门的性能分析工具(如 SmartPerf),可以精确测量启动时间的每个阶段。

能看到的信息

  • 进程创建时间
  • Ability 启动时间
  • 页面渲染时间
  • 每一帧的耗时
  • 有没有掉帧

手动打日志适合日常开发调试,性能分析工具适合深度优化。对大多数应用来说,手动打日志就够用了。

「民族图鉴」的启动时间数据

我们来看看「民族图鉴」实际的启动时间是什么水平:

阶段 耗时 说明
进程创建 + Ability 初始化 ~150ms 系统层面,应用大小正常
启动页创建 + 首次渲染 ~50ms 启动页很简单,渲染快
白屏总时长 ~200ms 用户几乎感知不到
启动页动画时长 2200ms 我们设计的时长
总启动时间(到首页) ~2500ms 包含动画展示时间

200ms 的白屏时间是什么水平?——优秀。一般来说,< 300ms 用户几乎感知不到,300-500ms 能感觉到但可以接受,> 500ms 就明显觉得慢了。「民族图鉴」200ms 左右,属于很好的水平。

启动时间优化建议

如果你的应用启动慢,可以从这几个方向优化:

1. Ability 初始化减负
// ❌ 不要在 onCreate 里做太多事
onCreate(want, launchParam) {
  this.initDatabase();      // 推迟
  this.initNetwork();       // 推迟
  this.initAnalytics();     // 推迟
  this.initThirdSDK();      // 推迟
}

// ✅ 只做最必要的事
onCreate(want, launchParam) {
  AppStorage.setOrCreate('appStartTimestamp', Date.now());
}

优化效果:能减少 100-500ms,视初始化工作量而定。

2. 启动页尽量简单
  • 少用图片,多用矢量图形和文字
  • 布局层级不要太深
  • 少用复杂组件

优化效果:能减少 50-200ms 的渲染时间。

3. 首页懒加载

首屏不需要的内容,可以延迟加载:

  • 非首屏 Tab 的内容,等用户切换到再加载
  • 图片资源,滚动到再加载
  • 复杂的计算,等首帧渲染完再算
4. 资源优化
  • 图片压缩,减少体积
  • 字体文件按需加载,不要全量加载
  • 减少启动时的资源加载量

启动优化的核心原则:首屏优先。只让用户看到的东西先加载,其他的都往后排。用户只关心他看到的那一页快不快,后面的内容慢点没关系。


🛠️ 核心实现

步骤1:动画基础——属性动画 vs 显式动画

在写代码之前,我们先搞清楚鸿蒙的两种动画方式:

1.1 属性动画(.animation() 装饰器)

属性动画是最常用的方式,给组件加一个 .animation() 属性,当关联的状态变量变化时,自动产生动画过渡。

@Component
struct Demo {
  @State opacity: number = 0;  // 关联的状态变量

  build() {
    Text('Hello')
      .opacity(this.opacity)    // 属性绑定状态
      .animation({              // 动画装饰器
        duration: 500,          // 动画时长
        curve: Curve.EaseInOut  // 动画曲线
      })
  }

  private start(): void {
    this.opacity = 1;  // 改一下状态,自动有动画
  }
}

特点

  • 声明式,写起来简单
  • 状态一变就动画,不用手动控制
  • 适合简单的属性变化动画
1.2 显式动画(animateTo 函数)

显式动画是用 animateTo() 函数包裹状态修改,指定动画参数。

@Component
struct Demo {
  @State width: number = 100;

  private start(): void {
    animateTo({
      duration: 500,
      curve: Curve.EaseOut
    }, () => {
      this.width = 200;  // 包裹在 animateTo 里的状态变化,会产生动画
    });
  }
}

特点

  • 命令式,更灵活
  • 可以精确控制什么时候开始动画
  • 适合复杂的、有条件的动画
1.3 怎么选?
场景 推荐方式 原因
简单的属性变化动画 属性动画(.animation) 写起来简单,声明式
多属性联动动画 属性动画(.animation) 一个 .animation 管所有属性
需要精确控制时机 显式动画(animateTo) 想什么时候调就什么时候调
有条件的动画 显式动画(animateTo) if 里调用 animateTo
动画序列 两种都行 属性动画 + setTimeout 更常用

「民族图鉴」启动页用的是属性动画 + setTimeout,因为启动页的动画是自动播放的,不需要用户交互,属性动画写起来更简洁。


步骤2:状态设计——哪些属性需要动画?

启动页有 4 个动画元素,我们需要 4 个状态变量:

// pages/SplashPage.ets
@Entry
@Component
struct SplashPage {
  // Logo的缩放比例(从0.5放大到1.0)
  @State logoScale: number = 0.5;
  
  // Logo的透明度(从0淡入到1)
  @State logoOpacity: number = 0;
  
  // 标题的透明度
  @State titleOpacity: number = 0;
  
  // 副标题的透明度
  @State subtitleOpacity: number = 0;
  
  // 底部进度条的宽度(从0延伸到80vp)
  @State progressWidth: number = 0;
  
  // ...
}

💡 为什么用 5 个状态变量,而不是更少? 因为每个元素的动画时机和时长都不一样。Logo 有缩放+淡入两个属性变化,但它们同时开始同时结束,可以共用一个时机,所以用两个变量分别控制缩放和透明度。标题和副标题的淡入时机不同,所以要分开。进度条是独立的,也要单独控制。


步骤3:动画序列编排——setTimeout 的妙用

启动页的动画是按时间顺序依次触发的,怎么实现?

setTimeout(),在不同的时间点修改不同的状态变量。

// pages/SplashPage.ets

/**
 * 启动页动画序列:Logo缩放淡入 → 标题淡入 → 副标题+装饰线淡入 → 进度条
 * 使用 setTimeout 触发状态变化,配合 .animation() 装饰器实现平滑过渡
 */
private startAnimation(): void {
  // 第一阶段:Logo放大淡入 (100ms后开始,持续800ms)
  setTimeout(() => {
    this.logoScale = 1.0;
    this.logoOpacity = 1;
  }, 100);

  // 第二阶段:标题淡入 (700ms后开始,持续600ms)
  setTimeout(() => {
    this.titleOpacity = 1;
  }, 700);

  // 第三阶段:副标题淡入 (1100ms后开始,持续600ms)
  setTimeout(() => {
    this.subtitleOpacity = 1;
  }, 1100);

  // 第四阶段:底部进度条动画 (1300ms后开始,持续700ms)
  setTimeout(() => {
    this.progressWidth = 80;
  }, 1300);
}

时间线可视化

时间(ms): 0    200   400   600   800  1000  1200  1400  1600  1800  2000  2200
          ├─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
Logo:     └────── 缩放淡入 ──────┘
标题:                 └────── 淡入 ──────┘
副标题:                       └────── 淡入 ──────┘
进度条:                             └──── 延伸 ────┘
跳转:                                                                   └─ 跳转

看到了吗?动画之间有重叠,这样节奏感更好,不会一个结束了下一个才开始,显得拖沓。

💡 为什么不用 Promise 链式调用? 也可以用 Promise + async/await 来编排动画序列,但 setTimeout 更直观,代码更简洁。启动页这种简单的动画序列,setTimeout 就够了。复杂的动画编排(比如动画 A 结束后动画 B 才开始,B 结束后 C 才开始),可以用 Promise 链。


步骤4:动画曲线——让动画更自然

动画不是匀速的,真实世界的运动都是有加速度的。动画曲线(Curve)决定了动画的速度变化。

4.1 常用动画曲线
曲线 效果 适用场景
Curve.Linear 匀速 旋转、进度条等需要匀速的动画
Curve.EaseIn 慢→快(加速) 元素飞出屏幕、淡出等
Curve.EaseOut 快→慢(减速) 元素进入屏幕、淡入等(最常用)
Curve.EaseInOut 慢→快→慢(先加速后减速) 元素在屏幕内移动、尺寸变化
Curve.FastOutSlowIn 快速开始,慢速结束 Material Design 标准曲线
Curve.Bounce 弹跳效果 强调、吸引注意力的动画
Curve.Spring 弹性效果 物理感的弹跳动画
4.2 怎么选曲线?

入门法则

  • 元素出现(进入屏幕):用 EaseOut(快→慢),因为用户期待它出现,快点来,然后慢慢停下
  • 元素消失(离开屏幕):用 EaseIn(慢→快),让它慢慢加速离开
  • 元素在屏幕内移动:用 EaseInOut(慢→快→慢),最自然
  • 进度条、旋转:用 Linear(匀速)

「民族图鉴」启动页的曲线选择

// Logo 缩放淡入:元素出现,用 EaseOut
.scale({ x: this.logoScale, y: this.logoScale })
.opacity(this.logoOpacity)
.animation({
  duration: 800,
  curve: Curve.EaseOut   // 放大淡入,先快后慢
})

// 标题淡入:也是元素出现,用 EaseOut
.opacity(this.titleOpacity)
.animation({
  duration: 600,
  curve: Curve.EaseOut   // 淡入,先快后慢
})

// 进度条延伸:用 EaseInOut
.width(this.progressWidth)
.animation({
  duration: 700,
  curve: Curve.EaseInOut // 进度条,先加速后减速
})

💡 为什么不用 Spring 弹跳? 民族图鉴是文化类应用,风格应该是典雅、稳重的。弹跳动画太活泼了,不搭。动画风格要和应用的调性一致——工具类应用可以利落干脆,游戏类应用可以活泼弹跳,文化类应用要舒缓优雅。


步骤5:完整页面实现

现在把所有内容拼起来,就是完整的启动页了:

// pages/SplashPage.ets
/**
 * 文件用途:启动页 - 品牌Logo动画 + 自动跳转首页
 * 创建时间:2026-06-20
 * 兼容环境:macOS/Linux/Docker/TRAE 云端
 * 版本:v1.0
 * 风险提示:无
 */
import router from '@ohos.router';

@Entry
@Component
struct SplashPage {
  // ========== 状态变量 ==========
  @State logoScale: number = 0.5;       // Logo缩放比例
  @State logoOpacity: number = 0;       // Logo透明度
  @State titleOpacity: number = 0;      // 标题透明度
  @State subtitleOpacity: number = 0;   // 副标题透明度
  @State progressWidth: number = 0;     // 进度条宽度

  // ========== 生命周期 ==========
  aboutToAppear(): void {
    this.startAnimation();
    this.navigateToHome();
  }

  // ========== 动画方法 ==========
  /**
   * 启动页四阶段动画序列
   * 1. Logo 缩放淡入(0.5→1.0,0→1)
   * 2. 标题淡入(0→1)
   * 3. 副标题淡入(0→1)
   * 4. 进度条延伸(0→80vp)
   */
  private startAnimation(): void {
    // 第一阶段:Logo放大淡入
    setTimeout(() => {
      this.logoScale = 1.0;
      this.logoOpacity = 1;
    }, 100);

    // 第二阶段:标题淡入
    setTimeout(() => {
      this.titleOpacity = 1;
    }, 700);

    // 第三阶段:副标题+装饰线淡入
    setTimeout(() => {
      this.subtitleOpacity = 1;
    }, 1100);

    // 第四阶段:底部进度条动画
    setTimeout(() => {
      this.progressWidth = 80;
    }, 1300);
  }

  /**
   * 延迟跳转到首页
   * 动画结束后停留 200ms,给用户一个缓冲
   */
  private navigateToHome(): void {
    setTimeout(() => {
      router.replaceUrl({ url: 'pages/Index' }).catch((err: Error) => {
        console.error('SplashPage navigate failed:', JSON.stringify(err));
      });
    }, 2200);
  }

  // ========== UI 构建 ==========
  build() {
    Column() {
      // ---------- Logo 区域 ----------
      Column() {
        Text('56')
          .fontSize(48)
          .fontWeight(FontWeight.Bold)
          .fontColor($r('app.color.text_on_primary'))
          .textAlign(TextAlign.Center)

        Text($r('app.string.splash_logo_sub'))
          .fontSize(16)
          .fontColor($r('app.color.text_on_primary'))
          .opacity(0.8)
          .margin({ top: 4 })
      }
      .width(120)
      .height(120)
      .borderRadius(60)
      .backgroundColor($r('app.color.primary_color'))
      .justifyContent(FlexAlign.Center)
      .scale({ x: this.logoScale, y: this.logoScale })
      .opacity(this.logoOpacity)
      .animation({
        duration: 800,
        curve: Curve.EaseOut
      })

      // ---------- 应用名称 ----------
      Text($r('app.string.splash_title'))
        .fontSize($r('app.float.font_size_xxxl'))
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .margin({ top: 40 })
        .opacity(this.titleOpacity)
        .animation({
          duration: 600,
          curve: Curve.EaseOut
        })

      // ---------- 副标题 ----------
      Text($r('app.string.splash_subtitle'))
        .fontSize($r('app.float.font_size_md'))
        .fontColor($r('app.color.text_secondary'))
        .margin({ top: 12 })
        .opacity(this.subtitleOpacity)
        .animation({
          duration: 600,
          curve: Curve.EaseOut
        })

      // ---------- 底部进度条 ----------
      Row()
        .width(this.progressWidth)
        .height(3)
        .borderRadius(1.5)
        .backgroundColor($r('app.color.primary_color'))
        .margin({ top: 24 })
        .opacity(this.subtitleOpacity)
        .animation({
          duration: 700,
          curve: Curve.EaseInOut
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.page_bg'))
    .justifyContent(FlexAlign.Center)
  }
}

步骤5.5:品牌Logo动画的深度设计——缩放、渐入、位移组合

「民族图鉴」的 Logo 动画看起来简单,但背后有很多设计考量。为什么是缩放+淡入,而不是别的?为什么时长是 800ms 而不是 500ms?为什么曲线用 EaseOut?这一节我们深入拆解 Logo 动画的设计思路。

Logo动画的设计目标

Logo 动画不只是"动起来就行",它有明确的设计目标:

目标 说明 怎么实现
吸引注意力 用户一打开就注意到 Logo 从中心放大,视觉焦点集中
品牌记忆 让用户记住品牌标识 完整展示 Logo,停留足够时间
品质感 让用户觉得"这个应用很精致" 流畅的动画、优雅的曲线
仪式感 文化类应用需要一点仪式感 舒缓的节奏、层层递进

很多开发者做启动页动画,喜欢加一堆炫酷效果——旋转、弹跳、粒子特效……结果反而喧宾夺主,用户看完只记得"动画很炫",记不住品牌。好的动画是"服务于品牌"的,不是"展示技术"的。

缩放动画的设计细节

「民族图鉴」的 Logo 用了缩放动画:从 0.5 倍放大到 1.0 倍。

为什么是缩放,而不是位移?

动画方式 效果 适用场景
从上方落下(位移) 有动感、活泼 游戏类、娱乐类应用
从中心放大(缩放) 聚焦、有冲击力 品牌展示、工具类
淡入(透明度) 优雅、舒缓 文化类、阅读类
旋转进入 俏皮、有趣 年轻化、个性化应用

缩放 + 淡入的组合,既有视觉冲击力(放大吸引注意力),又有优雅感(淡入不突兀),非常适合文化类应用的调性。

为什么从 0.5 开始,不是从 0 开始?

从0开始放大:0 → 0.1 → 0.3 → 0.5 → 0.7 → 0.9 → 1.0
                 ↑ 这一段用户几乎看不见

从0.5开始放大:0.5 → 0.6 → 0.7 → 0.8 → 0.9 → 1.0
                 ↑ 用户一进来就能看到变化
  • 从 0 开始:前 200ms Logo 太小了,用户几乎看不见,浪费了时间
  • 从 0.5 开始:用户一进来就看到 Logo 在放大,立刻有"动画开始了"的感觉
  • 0.5 是经验值——既能保证用户能看到,又有足够的放大空间

为什么用 EaseOut 曲线?

  • 元素出现的时候用 EaseOut(快→慢),这是动画设计的基本法则
  • 开始快:用户立刻看到变化,不觉得拖沓
  • 结束慢:稳稳地停在最终位置,有"落地"的感觉
  • 如果用 Linear(匀速),会显得很机械、不自然
  • 如果用 EaseIn(慢→快),结束的时候很突兀,像"飞出去了"

记住这个口诀:出现用 EaseOut,消失用 EaseIn,移动用 EaseInOut。这是动画设计的黄金法则,绝大多数场景都适用。

渐入动画的设计细节

透明度从 0 变到 1,叫"淡入"(Fade In)。

为什么要加淡入?只有缩放不行吗?

只有缩放的话,Logo 从 0.5 倍突然出现,然后放大——会有"蹦出来"的感觉,有点突兀。加上淡入之后,Logo 是"渐渐浮现"的,更柔和、更优雅。

缩放和淡入同时开始,同时结束——两个属性的动画时长都是 800ms,同步变化。这样视觉上是一个统一的"浮现+放大"效果,而不是两个分离的动画。

代码中的实现

// Logo 同时有缩放和透明度两个属性动画
.scale({ x: this.logoScale, y: this.logoScale })
.opacity(this.logoOpacity)
.animation({
  duration: 800,
  curve: Curve.EaseOut
})

// 两个状态变量同时修改,动画同步进行
setTimeout(() => {
  this.logoScale = 1.0;
  this.logoOpacity = 1;
}, 100);

因为两个属性用的是同一个 .animation() 装饰器,而且状态是同时改变的,所以它们天然就是同步的。这就是属性动画的好处——声明式,不用手动同步。

位移组合:如果想让 Logo 从上方落下怎么办?

「民族图鉴」用的是缩放+淡入,但如果你想做别的风格,比如 Logo 从上方落下 + 淡入,怎么实现?

代码示例

@Entry
@Component
struct DropDownSplash {
  @State logoY: number = -100;    // 初始位置:在上方100vp处
  @State logoOpacity: number = 0;

  aboutToAppear(): void {
    setTimeout(() => {
      animateTo({
        duration: 800,
        curve: Curve.EaseOut  // 落下也是"出现",用EaseOut
      }, () => {
        this.logoY = 0;       // 落到目标位置
        this.logoOpacity = 1; // 同时淡入
      });
    }, 100);
  }

  build() {
    Column() {
      Column() {
        Text('56')
          .fontSize(48)
          .fontColor(Color.White)
          .fontWeight(FontWeight.Bold)
      }
      .width(120)
      .height(120)
      .borderRadius(60)
      .backgroundColor('#C44569')
      .justifyContent(FlexAlign.Center)
      .offset({ y: this.logoY })        // 位移
      .opacity(this.logoOpacity)        // 透明度
      // 不用 .animation(),用 animateTo 显式动画也可以
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

效果:Logo 从上方 100vp 处落下,同时渐显,最后稳稳地停在中心。

offset 做位移动画,性能很好——因为 offset 属于 transform 范畴,是 GPU 加速的。不要用 marginposition 做位移动画,那会触发布局(Layout),性能差。

更多Logo动画组合参考

这里再给大家展示几种常见的 Logo 动画组合,供参考:

组合1:旋转 + 缩放 + 淡入(活泼风格)

@State logoScale: number = 0;
@State logoOpacity: number = 0;
@State logoRotate: number = -180;

animateTo({ duration: 1000, curve: Curve.EaseOut }, () => {
  this.logoScale = 1;
  this.logoOpacity = 1;
  this.logoRotate = 0;
});

// UI 上加上 rotate
.rotate({ angle: this.logoRotate })

适用:年轻化、个性化的应用。

组合2:脉冲缩放 + 淡入(呼吸感)

// 先放大到1.1倍,再弹回1.0倍,有呼吸感
animateTo({ duration: 600, curve: Curve.EaseOut }, () => {
  this.logoScale = 1.1;
  this.logoOpacity = 1;
});

setTimeout(() => {
  animateTo({ duration: 400, curve: Curve.EaseInOut }, () => {
    this.logoScale = 1.0;
  });
}, 600);

适用:品牌感强、有活力的应用。

组合3:左右两个元素向中间合拢(对称感)

@State leftX: number = -80;    // 左边元素初始在左边80vp
@State rightX: number = 80;    // 右边元素初始在右边80vp
@State opacity: number = 0;

animateTo({ duration: 800, curve: Curve.EaseOut }, () => {
  this.leftX = 0;
  this.rightX = 0;
  this.opacity = 1;
});

适用:有对称Logo、品牌名由两部分组成的应用。

动画组合有无数种,但核心原则只有一个:服务于品牌,不喧宾夺主。再炫的动画,如果用户看完记不住品牌,那就是失败的。

「民族图鉴」Logo动画设计复盘

最后,我们来复盘一下「民族图鉴」的 Logo 动画设计,看看每一个参数是怎么定的:

参数 为什么这么定
起始缩放 0.5 用户一进来就能看到变化,又有足够的放大空间
结束缩放 1.0 正常大小,清晰展示
起始透明度 0 从无到有,渐显效果
结束透明度 1 完全显示
动画时长 800ms 不短不长,既有仪式感又不拖沓
动画曲线 EaseOut 元素出现,快→慢,自然落地
开始延迟 100ms 页面渲染完稍微缓一下再开始,不仓促

整体效果

  • 用户打开应用 → 几乎立刻看到 Logo 从中心缓缓放大浮现
  • 放大速度先快后慢,稳稳地停在中心
  • 整个过程优雅、舒缓,符合文化类应用的调性
  • 用户看完记住了"56"这个圆形图腾 Logo

设计是"平衡的艺术"——动效太多太炫,喧宾夺主;动效太少太素,平淡无奇。找到那个平衡点,需要反复调试和打磨。这也是设计的乐趣所在。


步骤6:动画原理深入——属性动画是怎么工作的?

你可能会好奇:我只是改了一个状态变量,为什么就有动画了?中间的过渡帧是怎么来的?

让我们深入一下属性动画的工作原理:

6.1 动画系统的核心组件
状态变量变化
    ↓
动画系统检测到属性绑定的状态变了
    ↓
计算起始值 → 目标值的差值
    ↓
根据动画曲线,计算每一帧的中间值
    ↓
每一帧更新组件属性 → 渲染
    ↓
到达目标值 → 动画结束

举个例子

  • 起始值:opacity = 0
  • 目标值:opacity = 1
  • 时长:500ms
  • 曲线:EaseOut

动画系统会在 500ms 内,生成很多中间值:

0ms:    0    (0%)
50ms:   0.4  (快)
100ms:  0.6  (快)
200ms:  0.85 (开始变慢)
300ms:  0.95 (更慢)
400ms:  0.99 (几乎到了)
500ms:  1    (到了)

每一帧(通常 60 帧/秒,也就是每 16ms 一帧)更新一次,你看到的就是平滑的动画了。

6.2 哪些属性可以做动画?

几乎所有"可以连续变化"的属性都可以做动画:

类别 属性 示例
位置 offset、position、translate .offset({ x: 100, y: 0 })
尺寸 width、height、size .width(200)
缩放 scale .scale({ x: 1.5, y: 1.5 })
旋转 rotate .rotate({ angle: 45 })
透明度 opacity .opacity(0.5)
颜色 backgroundColor、fontColor .backgroundColor(Color.Red)
形状 borderRadius .borderRadius(20)
模糊 blur .blur(10)

不能做动画的:比如 Text 的文本内容、图片的 src 等"离散变化"的属性。

6.3 动画性能

动画为什么会流畅?因为很多动画是在GPU 层面做的,不需要每一帧都重新布局(Layout)和绘制(Paint)。

高性能动画属性(推荐多用):

  • transform(translate/scale/rotate)—— 完全在 GPU 做,性能最好
  • opacity—— 也很高效

低性能动画属性(尽量少用):

  • width/height—— 会触发布局(Layout),性能差
  • backgroundColor—— 会触发重绘(Paint),还行
  • 复杂的形状变化—— 性能较差

💡 「民族图鉴」启动页的性能考虑:Logo 的缩放用的是 scale(高性能),不是 width/height(低性能)。淡入淡出用 opacity(高性能)。只有进度条用 width(没办法,必须变宽度),但只有一个,没问题。


⚠️ 常见问题与解决方案

问题1:启动页动画不流畅,有卡顿

现象
第一次打开应用,启动页动画卡一下,或者有时候流畅有时候卡。

原因分析

可能原因 说明
应用初始化太重 aboutToAppear 里做了太多事,主线程忙,动画掉帧
图片太大 Logo 用了很大的图片,解码耗时
动画属性太多 同时有太多属性在动画,GPU 处理不过来
模拟器性能差 模拟器本身卡,真机可能没问题

解决方案

1. 把初始化工作推迟到首页

// ❌ 不要在启动页做重活
aboutToAppear(): void {
  this.initDataBase();      // 数据库初始化,慢
  this.initNetwork();       // 网络请求,慢
  this.startAnimation();    // 动画
  this.navigateToHome();
}

// ✅ 启动页只放动画,重活放首页
aboutToAppear(): void {
  this.startAnimation();    // 只有动画
  this.navigateToHome();
}

// 首页的 aboutToAppear 里做初始化

2. Logo 尽量用简单图形,不要用大图

// ✅ 我们的启动页是用 Text + 圆形背景做的,不是图片,性能最好
Column() {
  Text('56')
  Text('民族')
}
.width(120)
.height(120)
.borderRadius(60)
.backgroundColor($r('app.color.primary_color'))

3. 控制同时动画的属性数量

同时有 2-3 个动画很正常,同时有 10 个以上可能就卡了。能合并的合并,能错开的错开。

4. 用性能好的动画属性

// ❌ 用 width/height 做缩放(性能差,会触发 Layout)
.width(this.logoWidth)
.height(this.logoHeight)
.animation(...)

// ✅ 用 scale 做缩放(性能好,GPU 直接处理)
.scale({ x: this.logoScale, y: this.logoScale })
.animation(...)

问题2:setTimeout 在后台还在跑,导致跳转时机不对

现象
启动页动画过程中,按了 Home 键退到后台,过一会再进来,直接跳到首页了。动画还没播完呢。

原因
setTimeout 是基于系统时间的,应用在后台的时候,计时还在走。等你切回来,时间到了,就直接跳转了。

解决方案

用页面生命周期控制—— onPageHide 暂停,onPageShow 恢复

@Entry
@Component
struct SplashPage {
  private startTime: number = 0;     // 动画开始时间
  private elapsedTime: number = 0;   // 已经过了多久
  private timerId: number = -1;      // 定时器ID

  aboutToAppear(): void {
    this.startAnimation();
  }

  onPageShow(): void {
    // 页面显示时,恢复计时
    if (this.elapsedTime > 0) {
      this.resumeAnimation();
    }
  }

  onPageHide(): void {
    // 页面隐藏时,暂停计时
    this.elapsedTime = Date.now() - this.startTime;
    clearTimeout(this.timerId);
  }

  private startAnimation(): void {
    this.startTime = Date.now();
    // ... 启动动画
  }

  private resumeAnimation(): void {
    // 根据已经过去的时间,计算剩余时间,继续动画
    const remaining = 2200 - this.elapsedTime;
    if (remaining > 0) {
      // 还没到时间,继续等
      this.timerId = setTimeout(() => {
        this.navigateToHome();
      }, remaining);
    } else {
      // 已经过了时间,直接跳转
      this.navigateToHome();
    }
  }
}

💡 这个问题需要处理吗? 看产品需求。大多数应用的启动页其实不处理这个问题——用户切到后台再切回来,直接进入首页也很正常。毕竟启动页只是过渡,用户既然切出去了,说明他也不怎么想看启动页。如果你的产品有特殊要求(比如必须看完动画),那就加上暂停/恢复逻辑。


问题3:动画结束了,但页面还没加载好,白屏

现象
启动页动画结束,跳到首页,首页一片空白,过了一会才出来。

原因
首页数据多、组件复杂,第一次渲染慢。启动页动画 2 秒播完了,首页还没准备好。

解决方案

方案1:等首页准备好再跳转(推荐)

让启动页等首页加载完成再跳转,而不是死等 2 秒。

// 在启动页,等首页数据加载完成再跳
aboutToAppear(): void {
  this.startAnimation();  // 开始播动画
  
  // 同时开始预加载首页数据
  PreloadService.loadHomeData().then(() => {
    // 数据加载完了,但动画还没播完,等动画播完再跳
    const elapsed = Date.now() - this.startTime;
    const minDuration = 1500;  // 最短展示时间,避免闪一下就没了
    
    if (elapsed >= minDuration) {
      this.navigateToHome();
    } else {
      setTimeout(() => {
        this.navigateToHome();
      }, minDuration - elapsed);
    }
  });
}

方案2:首页加个骨架屏

如果首页加载确实慢,与其白屏,不如放个骨架屏(Skeleton Screen)占位,告诉用户"内容正在加载"。

// 首页
@Entry
@Component
struct HomePage {
  @State isLoading: boolean = true;

  build() {
    if (this.isLoading) {
      // 骨架屏:灰色的占位矩形,有淡入淡出动画
      SkeletonScreen()
    } else {
      // 真实内容
      HomeContent()
    }
  }
}

方案3:启动页时长调长一点

最简单的办法——把 2 秒改成 3 秒。但不推荐,因为:

  • 高端机加载快,用户要多等 1 秒,体验差
  • 低端机可能 3 秒还不够,还是会白屏

所以最好的方案是方案1:等数据加载完成再跳,同时设置一个最短展示时间(比如 1 秒),避免闪一下就没了。


问题4:多次进入启动页,动画重复播放

现象
从首页按返回键,又回到启动页了,动画又播一遍。或者打开了很多次启动页。

原因
用了 router.pushUrl() 跳转,启动页还在页面栈里。按返回键就回来了。

解决方案

用 replaceUrl 而不是 pushUrl

// ❌ pushUrl:启动页还在栈里,按返回会回来
router.pushUrl({ url: 'pages/Index' });

// ✅ replaceUrl:替换当前页,启动页不在栈里了
router.replaceUrl({ url: 'pages/Index' });

replaceUrl 的意思是:用新页面替换当前页面,当前页面出栈,新页面入栈。这样页面栈里就没有启动页了,按返回键不会回到启动页。

💡 启动页必须用 replaceUrl,这是基本常识。还有登录页也是——登录成功后要用 replaceUrl 跳到首页,不能让用户按返回又回到登录页。


问题5:启动页跳转动画太生硬

现象
启动页跳转首页的时候,"啪"一下就切过去了,很生硬。

原因
默认的页面跳转有过渡动画,但启动页到首页的切换可能不明显,或者风格不搭。

解决方案

方案1:自定义页面转场动画

// 首页的 aboutToAppear 里做个淡入动画
@Entry
@Component
struct Index {
  @State pageOpacity: number = 0;

  aboutToAppear(): void {
    animateTo({
      duration: 500,
      curve: Curve.EaseInOut
    }, () => {
      this.pageOpacity = 1;
    });
  }

  build() {
    Tabs() {
      // ...
    }
    .opacity(this.pageOpacity)
  }
}

方案2:启动页末尾加个淡出

在跳转前,让启动页整体淡出,然后再跳转。

// 启动页
@State pageOpacity: number = 1;

private navigateToHome(): void {
  setTimeout(() => {
    // 先淡出 300ms
    animateTo({
      duration: 300,
      curve: Curve.EaseIn
    }, () => {
      this.pageOpacity = 0;
    });
    
    // 淡出结束后跳转
    setTimeout(() => {
      router.replaceUrl({ url: 'pages/Index' });
    }, 300);
  }, 2000);
}

build() {
  Column() {
    // ... 内容
  }
  .opacity(this.pageOpacity)  // 整体透明度
}

当然,大多数情况下,默认的转场动画就够了。只有对体验要求特别高的应用,才会自定义启动页到首页的过渡。「民族图鉴」用默认的就挺好,不折腾。


📝 本章小结

核心知识点

本文深入讲解了启动页的设计与实现,以及鸿蒙动画系统的基础知识:

1. 启动页的作用

  • 品牌展示:建立第一印象
  • 加载缓冲:填补应用初始化的空白
  • 氛围营造:奠定应用的设计调性
  • 时长建议:1.5-2.5 秒,太短没存在感,太长用户烦

2. 两种动画方式

  • 属性动画(.animation()):声明式,简单,适合简单动画
  • 显式动画(animateTo()):命令式,灵活,适合复杂动画
  • 选择原则:简单的用属性动画,复杂的用显式动画

3. 动画序列编排

  • 用 setTimeout 在不同时间点修改状态,触发动画
  • 动画之间要有重叠,节奏感更好
  • 复杂序列可以用 Promise + async/await

4. 动画曲线

  • EaseOut:快→慢,元素出现时用(最常用)
  • EaseIn:慢→快,元素消失时用
  • EaseInOut:慢→快→慢,屏幕内移动用
  • Linear:匀速,进度条、旋转用
  • 选曲线的原则:和应用调性一致

5. 动画性能

  • 高性能属性:scale、translate、opacity(GPU 处理)
  • 低性能属性:width、height(触发布局)
  • 启动页 Logo 用 scale 不用 width/height

6. 启动页最佳实践

  • 用 replaceUrl 跳转,不要留在页面栈里
  • 启动页不要做重活,初始化放首页
  • 动画风格要和应用调性一致
  • 图片尽量简单,减少解码耗时

最佳实践总结

启动页跳转必须用 replaceUrl

// ✅ 正确:替换当前页,返回键不会回来
router.replaceUrl({ url: 'pages/Index' });

// ❌ 错误:push 到栈里,按返回会回到启动页
router.pushUrl({ url: 'pages/Index' });

动画属性优先用 transform 和 opacity

// ✅ 高性能:用 scale 缩放
.scale({ x: 1.5, y: 1.5 })
.opacity(0.5)

// ❌ 低性能:用宽高做缩放
.width(150)
.height(150)

元素出现用 EaseOut,元素消失用 EaseIn

// 淡入(出现):EaseOut
.opacity(this.opacity)
.animation({ duration: 500, curve: Curve.EaseOut })

// 淡出(消失):EaseIn
.opacity(this.opacity)
.animation({ duration: 500, curve: Curve.EaseIn })

动画之间要有重叠,节奏更紧凑

不要:A 完了 B 才开始,拖沓
应该:A 快结束时 B 就开始,紧凑

启动页要轻,初始化放首页

// 启动页只做一件事:展示动画 + 跳转
aboutToAppear(): void {
  this.startAnimation();
  this.navigateToHome();
}

下一篇预告

启动页搞定了,接下来该进入真正的首页了。

下一篇(第12篇)我们将讲解首页框架——Tabs 容器与自定义 TabBar

  • Tabs 组件的原理与使用
  • 自定义 TabBar 的设计与实现
  • Tab 切换动画与交互细节
  • 页面懒加载与性能优化

Tabs 是几乎所有应用都有的核心组件,掌握它,你就掌握了应用的主框架。


🔗 相关链接


💡 提示:启动页是用户对你应用的第一印象,值得花心思做好。但也要注意——启动页只是过渡,不要喧宾夺主。好的启动页是"恰到好处"——不长不短,不炫不素,用户看完觉得"这个应用看起来挺专业的",然后就顺畅地进入主界面了。这才是启动页的真正价值。

Logo

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

更多推荐