HarmonyOS应用<民族图鉴>开发第11篇:启动页设计——品牌Logo与动画序列深度解析

📖 引言
打开任何一个 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 不做重活",白屏时间就已经很短了,用户几乎感知不到。
方案四:「民族图鉴」的实践
「民族图鉴」是怎么做的呢?我们用了组合方案:
- Ability 背景色和启动页一致:从根源上消除白屏感
- 启动页保持简洁:用 Text + Column 实现,不用图片,渲染快
- 初始化推迟到首页:启动页只放动画,不做任何重活
- 动画立即开始:aboutToAppear 里马上触发动画,不等待
// 启动页的 aboutToAppear,轻量到极致
aboutToAppear(): void {
this.startAnimation(); // 只启动动画
this.navigateToHome(); // 只开始计时
}
实际效果是:用户点击图标 → 瞬间看到和启动页同色的背景 → 几乎立即就看到 Logo 开始放大 → 完整动画序列 → 进入首页。整个过程流畅自然,完全感觉不到白屏。
💡 白屏一定需要解决吗? 不一定。如果你的应用启动很快(< 100ms),用户根本感知不到白屏,那就没必要专门处理。但如果启动慢,白屏时间超过 200ms,用户就能明显感觉到"卡了一下",这时候就需要优化了。
📊 启动页的三种实现方式对比
实现启动页,不止一种方式。鸿蒙生态中,常见的有三种实现方式:Ability 样式方式、页面动画方式、系统 Splash 组件方式。各有优劣,适合不同场景。
方式一:Ability 样式方式(系统级)
原理:通过配置 Ability 的主题、启动窗口背景、启动图标,让系统在应用启动时显示一个静态的启动画面。应用准备好之后,系统自动关闭启动窗口,显示应用内容。
实现方式:
- 在
module.json5中配置startWindowBackground、startWindowIcon - 纯配置,不需要写代码
- 系统层面实现
{
"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 组件 |
|---|---|---|---|
| 启动速度 | ⭐⭐⭐⭐⭐ 最快 | ⭐⭐⭐ 中等 | ⭐⭐⭐ 中等 |
| 白屏问题 | ⭐⭐⭐⭐⭐ 完全没有 | ⭐⭐⭐ 需要优化 | ⭐⭐⭐ 需要优化 |
| 动画效果 | ⭐ 只有静态 | ⭐⭐⭐⭐⭐ 完全自定义 | ⭐⭐⭐ 有限 |
| 开发成本 | ⭐ 最低(配置就行) | ⭐⭐⭐ 中等 | ⭐⭐ 较低 |
| 定制化程度 | ⭐ 很低 | ⭐⭐⭐⭐⭐ 最高 | ⭐⭐⭐ 中等 |
| 包体积影响 | ⭐⭐⭐⭐⭐ 无影响 | ⭐⭐⭐⭐ 很小 | ⭐⭐⭐ 有一点 |
| 推荐指数 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
「民族图鉴」的选择理由
我们为什么选择了页面动画方式?
- 动画需求:我们需要四阶段动画序列(Logo 缩放 → 标题淡入 → 副标题淡入 → 进度条),静态启动图实现不了
- 品牌调性:文化类应用需要有仪式感,优雅的动画能提升品牌质感
- 控制能力:可以精确控制每个元素的动画时机、时长、曲线
- 可扩展性:以后要加开屏广告、节日启动页、版本更新引导,都很方便
- 白屏可优化:通过 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 加速的。不要用margin或position做位移动画,那会触发布局(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 是几乎所有应用都有的核心组件,掌握它,你就掌握了应用的主框架。
🔗 相关链接
- 项目源码: GitCode 仓库
- 启动页源码: [SplashPage.ets](file:///e:/HuaweiDevEco/Project/project003/entry/src/main/ets/pages/SplashPage.ets)
- 属性动画: 官方文档
- 显式动画: 官方文档
- 动画曲线: 官方文档
- 页面路由: 官方文档
💡 提示:启动页是用户对你应用的第一印象,值得花心思做好。但也要注意——启动页只是过渡,不要喧宾夺主。好的启动页是"恰到好处"——不长不短,不炫不素,用户看完觉得"这个应用看起来挺专业的",然后就顺畅地进入主界面了。这才是启动页的真正价值。
更多推荐

所有评论(0)