鸿蒙实战:入场动画设计 --- 分段式页面入场
前言(必读)

图:鸿蒙实战:入场动画设计 --- 分段式页面入场(第 59 篇) 运行效果截图(HarmonyOS NEXT)
用户打开一个页面时,如果所有元素同时出现,会显得「生硬」和「廉价」。通过分段式入场动画------页面元素按优先级依次入场------可以显著提升应用的精致感和用户体验。本文以 HomePage(5 段入场)和 ReportDetailPage(5 段入场链)为核心案例,深入讲解分段式入场的设计与实现。

图:分段式入场动画------5个元素依次延迟入场,形成层次感
底部导航 功能按钮 统计/数据区 核心内容区 顶部标题栏 页面 onPageShow 底部导航 功能按钮 统计/数据区 核心内容区 顶部标题栏 页面 onPageShow #mermaid-svg-w2g0gpr1AsQoMwJk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-w2g0gpr1AsQoMwJk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-w2g0gpr1AsQoMwJk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-w2g0gpr1AsQoMwJk .error-icon{fill:#552222;}#mermaid-svg-w2g0gpr1AsQoMwJk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-w2g0gpr1AsQoMwJk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-w2g0gpr1AsQoMwJk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-w2g0gpr1AsQoMwJk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-w2g0gpr1AsQoMwJk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-w2g0gpr1AsQoMwJk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-w2g0gpr1AsQoMwJk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-w2g0gpr1AsQoMwJk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-w2g0gpr1AsQoMwJk .marker.cross{stroke:#333333;}#mermaid-svg-w2g0gpr1AsQoMwJk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-w2g0gpr1AsQoMwJk p{margin:0;}#mermaid-svg-w2g0gpr1AsQoMwJk .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-w2g0gpr1AsQoMwJk text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-w2g0gpr1AsQoMwJk .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-w2g0gpr1AsQoMwJk .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-w2g0gpr1AsQoMwJk .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-w2g0gpr1AsQoMwJk .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-w2g0gpr1AsQoMwJk #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-w2g0gpr1AsQoMwJk .sequenceNumber{fill:white;}#mermaid-svg-w2g0gpr1AsQoMwJk #sequencenumber{fill:#333;}#mermaid-svg-w2g0gpr1AsQoMwJk #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-w2g0gpr1AsQoMwJk .messageText{fill:#333;stroke:none;}#mermaid-svg-w2g0gpr1AsQoMwJk .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-w2g0gpr1AsQoMwJk .labelText,#mermaid-svg-w2g0gpr1AsQoMwJk .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-w2g0gpr1AsQoMwJk .loopText,#mermaid-svg-w2g0gpr1AsQoMwJk .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-w2g0gpr1AsQoMwJk .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-w2g0gpr1AsQoMwJk .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-w2g0gpr1AsQoMwJk .noteText,#mermaid-svg-w2g0gpr1AsQoMwJk .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-w2g0gpr1AsQoMwJk .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-w2g0gpr1AsQoMwJk .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-w2g0gpr1AsQoMwJk .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-w2g0gpr1AsQoMwJk .actorPopupMenu{position:absolute;}#mermaid-svg-w2g0gpr1AsQoMwJk .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-w2g0gpr1AsQoMwJk .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-w2g0gpr1AsQoMwJk .actor-man circle,#mermaid-svg-w2g0gpr1AsQoMwJk line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-w2g0gpr1AsQoMwJk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 每个元素 opacity 0→1 + translateY 20→0 delay=0ms 立即入场 delay=100ms delay=200ms delay=300ms delay=400ms
一、入场动画的核心设计原则
1.1 分段原则
人眼对内容的感知顺序:
「重点先看,装饰后看」
分段优先级:
1. 核心内容 → 最先出现(如欢迎语)
2. 交互入口 → 稍后出现(如「拍一张笔迹」按钮)
3. 辅助内容 → 中间出现(如快捷入口)
4. 次级内容 → 较晚出现(如列表/卡片)
5. 装饰内容 → 最后出现(如灵感卡片/品牌元素)
1.2 常用入场参数
| 参数 | 含义 | HomePage 值 | 效果 |
|---|---|---|---|
opacity: 0→1 |
透明度渐入 | 500ms | 平滑出现 |
translate.y: 20→0 |
从下方移入 | 500ms | 微上升感 |
duration |
动画时长 | 400-500ms | 不快不慢 |
staggerDelay |
段间距 | 150-200ms | 明显分段 |
curve |
缓动函数 | FastOutSlowIn | 先快后慢 |
二、HomePage 五段入场动画
2.1 完整实现
typescript
// 文件:pages/HomePage.ets(入场动画相关状态和逻辑)
// ===== 8 个入场状态变量 =====
@State welcomeOpacity: number = 0 // 欢迎区透明度
@State welcomeOffset: number = 20 // 欢迎区 Y 偏移
@State heroOpacity: number = 0 // Hero CTA 透明度
@State heroOffset: number = 20 // Hero CTA Y 偏移
@State subEntryOpacity: number = 0 // 快捷入口透明度
@State recentOpacity: number = 0 // 最近报告透明度
@State recentOffset: number = 20 // 最近报告 Y 偏移
@State inspireOpacity: number = 0 // 灵感卡片透明度
2.2 入场编排
typescript
private startEntranceAnimation(): void {
// 段 1:欢迎区(opacity + translate)
animateTo({ duration: 500, curve: Curve.FastOutSlowIn }, () => {
this.welcomeOpacity = 1
this.welcomeOffset = 0
})
// 段 2:Hero CTA(延迟 200ms)
setTimeout(() => {
animateTo({ duration: 500, curve: Curve.FastOutSlowIn }, () => {
this.heroOpacity = 1
this.heroOffset = 0
})
}, 200)
// 段 3:快捷入口(仅 opacity)
setTimeout(() => {
animateTo({ duration: 400, curve: Curve.FastOutSlowIn }, () => {
this.subEntryOpacity = 1
})
}, 400)
// 段 4:最近报告(opacity + translate)
setTimeout(() => {
animateTo({ duration: 500, curve: Curve.FastOutSlowIn }, () => {
this.recentOpacity = 1
this.recentOffset = 0
})
}, 550)
// 段 5:灵感卡片(仅 opacity)
setTimeout(() => {
animateTo({ duration: 400, curve: Curve.FastOutSlowIn }, () => {
this.inspireOpacity = 1
})
}, 750)
}
2.3 时序图
时间 (ms) 0 200 400 550 750 950 1150
│ │ │ │ │ │ │
段 1 欢迎区 ████████
(opacity+ opacity+offset → 1/0
translate)
段 2 Hero ░░░░░░░░████████
(opacity+ delay 200ms → opacity+offset → 1/0
translate)
段 3 快捷 ░░░░░░░░░░░░░░████
(opacity) delay 400ms → opacity → 1
段 4 最近 ░░░░░░░░░░░░░░░░░░████████
(opacity+ delay 550ms → opacity+offset → 1/0
translate)
段 5 灵感 ░░░░░░░░░░░░░░░░░░░░░░░░████
(opacity) delay 750ms → opacity → 1
2.4 为什么要组合 opacity + translate?
单一属性动画往往不够「有质感」。通过组合两个属性可以实现更丰富的视觉效果:
| 组合 | 视觉感受 | 适用区域 |
|---|---|---|
opacity 单独 |
渐显,较平 | 非核心装饰元素 |
translate + opacity |
从下方升起渐显 | 核心元素(推荐) |
scale + opacity |
从中心放大渐显 | 品牌 logo / Hero 区域 |
translate.x + opacity |
从侧面滑入 | 抽屉式元素 |
三、ReportDetailPage 的五段入场链
3.1 onFinish 链式实现
typescript
// 文件:pages/ReportDetailPage.ets
// 使用 onFinish 回调链而非 setTimeout
private startEntranceAnimation(): void {
animateTo({
duration: AppAnimations.ENTER_FAST_DURATION,
curve: AppAnimations.CURVE_SMOOTH,
onFinish: () => {
// 段 2: 情绪卡片
animateTo({
duration: AppAnimations.ENTER_FAST_DURATION,
curve: AppAnimations.CURVE_SMOOTH,
onFinish: () => {
// 段 3: 雷达+5点制
animateTo({
duration: AppAnimations.ENTER_FAST_DURATION,
curve: AppAnimations.CURVE_SMOOTH,
onFinish: () => {
// 段 4: AI 综合解读
animateTo({
duration: AppAnimations.ENTER_FAST_DURATION,
curve: AppAnimations.CURVE_SMOOTH,
onFinish: () => {
// 段 5: 操作按钮(dotOffset 动画)
animateTo({
duration: AppAnimations.ENTER_DURATION + 100,
curve: AppAnimations.CURVE_SMOOTH
}, () => { this.dotOffset = 1 })
}
}, () => { this.aiOpacity = 1; this.aiOffset = 0 })
}
}, () => { this.radarOpacity = 1; this.radarOffset = 0 })
}
}, () => { this.moodOpacity = 1; this.moodOffset = 0 })
}
}, () => { this.srcOpacity = 1; this.srcOffset = 0 })
// 段 5 的按钮部分(独立 setTimeout)
setTimeout(() => {
animateTo({ duration: AppAnimations.ENTER_FAST_DURATION, curve: AppAnimations.CURVE_SMOOTH }, () => {
this.btnOpacity = 1
})
}, 1600)
}
3.2 两种串行方式的对比
| 方案 | ReportDetailPage(onFinish 链) | HomePage(setTimeout) |
|---|---|---|
| 精度 | 前一段完成才启动下一段 | 固定延迟时间 |
| 代码结构 | 嵌套 5 层(回调地狱) | 平铺 5 段(清晰) |
| 动态时长 | ✅ 适应不同卡片加载时间 | ❌ 固定时间偏移 |
| 推荐场景 | 需要精确衔接的场景 | 已知固定时长的场景 |
3.3 状态变量映射
UI 区域 @State 变量 动画方式 时序(ms)
────────────────────────────────────────────────────────────────
来源卡片 srcOpacity + srcOffset opacity+translate 0-400
情绪卡片 moodOpacity + moodOffset opacity+translate 400-800
雷达+5点制 radarOpacity + radarOffset opacity+translate 800-1200
综合解读 aiOpacity + aiOffset opacity+translate 1200-1600
操作按钮 btnOpacity opacity 1600-2000
滑块指示器 dotOffset x 轴位置 1600-2200
四、TrendPage 的三层入场设计
TrendPage 展示了另一种入场模式------层叠入场 + 关键词逐字弹出:
typescript
// 文件:pages/TrendPage.ets
// Layer 1:整体内容区域
@State contentOpacity: number = 0
@State contentOffset: number = 30
private startEntranceAnimation(): void {
// 整页内容淡入上升
animateTo({
duration: AppAnimations.ENTER_DURATION,
curve: AppAnimations.CURVE_SMOOTH
}, () => {
this.contentOpacity = 1
this.contentOffset = 0
})
}
// Layer 2:关键词逐字弹出(数据加载完成后触发)
afterDataLoaded(): void {
setTimeout(() => {
for (let i = 0; i < this.keywords.length; i++) {
setTimeout(() => {
animateTo({
duration: AppAnimations.ENTER_STAGGER_DURATION,
curve: AppAnimations.CURVE_EASE
}, () => {
const arr = [...this.keywordOpacities]
arr[i] = 1
this.keywordOpacities = arr
})
}, i * 80) // 每个关键词间隔 80ms
}
}, 900) // 主内容入场完成后延迟 900ms
}
// Layer 3:AI 月度小结卡片
afterDataLoaded(): void {
setTimeout(() => {
animateTo({
duration: AppAnimations.ENTER_DURATION + 100,
curve: AppAnimations.CURVE_SMOOTH
}, () => { this.summaryOpacity = 1 })
}, 1200)
}
三层入场时序
时间 (ms) 0 500 900 1000 1100 1200 1700
│ │ │ │ │ │ │
Layer 1 ████████████████
整页内容 opacity+offset → 1/0
Layer 2 ░░░░░░░░░░░░░░░░██████ ██ ██ ██ ██ ██ ██
关键词 延迟 900ms → 逐个 80ms 间隔弹出(6 个词约 480ms)
Layer 3 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░████████████
小结卡片 延迟 1200ms → opacity → 1
五、AnalyzingPage 的墨水动画入场
AnalyzingPage 的入场动画不是简单的 opacity + translate,而是三阶段墨水动画(原型:ink-drop + ink-spread):
typescript
// 文件:pages/AnalyzingPage.ets(墨水动画)
@State dropOffset: number = -70 // 墨滴 Y 位置
@State dropOpacity: number = 0 // 墨滴透明度
@State spreadOpacity: number = 0 // 墨迹扩散透明度
@State spreadScale: number = 0.5 // 墨迹扩散比例
private startInkAnimation() {
const drop = () => {
this.dropOffset = -70
this.dropOpacity = 0
this.spreadOpacity = 0
this.spreadScale = 0.5
// Phase 1:墨滴下落 (1000ms)
animateTo({
duration: 1000, curve: Curve.EaseIn,
onFinish: () => {
// Phase 2:墨迹扩散 (600ms)
animateTo({
duration: 600, curve: Curve.EaseOut,
onFinish: () => {
// Phase 3:渐隐 (400ms) → 循环
animateTo({
duration: 400, curve: Curve.EaseOut,
onFinish: () => drop() // 递归循环
}, () => {
this.spreadOpacity = 0
this.dropOpacity = 0
})
}
}, () => {
this.spreadOpacity = 0.25
this.spreadScale = 2.0
})
}
}, () => {
this.dropOffset = 0
this.dropOpacity = 1
})
}
setTimeout(() => drop(), 300)
}
三段动画时序:
下落 (1000ms) 扩散 (600ms) 渐隐 (400ms)
████████████████████ ██████████████ ████████
opacity 0→1 opacity 0→0.25 opacity→0
offset -70→0 scale 0.5→2.0
六、入场动画设计模式总结
6.1 四种入场模式
| 模式 | 描述 | 使用页面 | 状态变量数 |
|---|---|---|---|
| 平铺分段 | setTimeout 延迟调用多个 animateTo | HomePage | 5 对 |
| onFinish 链 | 嵌套回调,严格串行 | ReportDetailPage | 5 对 |
| 层叠+逐字 | 整体入场 + 子元素逐字弹出 | TrendPage | 变量级 |
| 三阶段循环 | Phase1→2→3→循环的复合动画 | AnalyzingPage | 4 个 |
6.2 状态变量命名规范
typescript
// 统一命名模式:[区域名] + [属性]
// 推荐:
@State welcomeOpacity: number = 0 // 区域名 + 动画属性
@State welcomeOffset: number = 20
@State heroOpacity: number = 0
@State heroOffset: number = 20
// 不推荐:
@State wOpa: number = 0 // 缩写,可读性差
@State wOff: number = 20
6.3 入场动画检查清单
| 检查项 | 要点 |
|---|---|
| 初始值设定 | @State 初始值为动画起始值(opacity=0, offset=20) |
| 触发时机 | aboutToAppear 或数据加载完成后触发 |
| 绑定 UI | UI 元素上绑定 opacity() 和 translate() |
| 分段间隔 | 150-200ms 间距保证分段感知 |
| 时长控制 | 核心内容 500ms,次要 400ms |
| 曲线选择 | 核心用 FastOutSlowIn,装饰用 EaseOut |
七、最佳实践
typescript
// 入场动画的标准模板
@Entry
@Component
struct MyPage {
// 1. 定义入场状态变量
@State section1Opacity: number = 0
@State section1Offset: number = 20
@State section2Opacity: number = 0
aboutToAppear() {
// 2. 触发入场
setTimeout(() => this.startEntrance(), 50)
}
// 3. 入场方法
private startEntrance(): void {
// 段 1:核心内容
animateTo({ duration: 500, curve: Curve.FastOutSlowIn }, () => {
this.section1Opacity = 1
this.section1Offset = 0
})
// 段 2:次要内容(延迟 200ms)
setTimeout(() => {
animateTo({ duration: 400, curve: Curve.FastOutSlowIn }, () => {
this.section2Opacity = 1
})
}, 200)
}
build() {
Column() {
Section1()
.opacity(this.section1Opacity)
.translate({ y: this.section1Offset })
Section2()
.opacity(this.section2Opacity)
}
}
}
| 实践 | 建议 | 原因 |
|---|---|---|
| 初始 offset | 20-30px 足够 | 太大显得突兀,太小感知不到 |
| 最短时长 | 400ms | 低于 300ms 人眼容易忽略 |
| 最长时长 | 500ms | 超过 600ms 会感觉等待 |
| 分段间距 | 150-200ms | 以太快不分段,太慢脱节 |
| 组合属性 | opacity + translate 是黄金组合 | 比单 opacity 丰富,比 scale 自然 |
八、注意事项与常见问题
8.1 开发注意事项
说明: 以下注意事项基于 HarmonyOS NEXT 实际项目开发经验整理,建议在动手开发前仔细阅读,可有效避免常见坑点。
在实际开发过程中,需特别注意以下几点:
- API 兼容性:部分接口仅在特定 HarmonyOS NEXT 版本中可用,需做版本条件判断
- 权限模型:采用静态声明(module.json5)+ 动态申请(requestPermissionsFromUser)的两阶段授权
- 生命周期 :合理使用
aboutToAppear()和aboutToDisappear()管理资源初始化与释放 - 状态同步 :跨页面数据通过 AppStorage 共享,组件内状态使用
@State/@Prop/@Link装饰器
8.2 常见错误与解决方案
常见问题快速排查表:
| 问题类型 | 排查方向 | 参考方法 |
|---|---|---|
| 应用崩溃 | 查看 hilog 错误日志 | hilog.error(TAG, "...", e.message) |
| 状态丢失 | 检查 AppStorage 键名拼写 | 统一使用常量管理键名 |
| 动画不流畅 | 避免在 animateTo 回调中执行 I/O | 动画与数据操作分离 |
提示: 建议在 DevEco Studio 中开启 ArkTS Lint 静态检查,大部分编译期问题可在开发阶段发现。
总结
本文深入讲解了分段式入场动画的设计与实现:
- ✅ 核心原则:核心内容先出现,装饰内容后出现
- ✅ HomePage 5 段入场:setTimeout 平铺编排,opacity + translate 组合
- ✅ ReportDetailPage 5 段入场链:onFinish 回调嵌套,严格串行衔接
- ✅ TrendPage 三层入场:整体淡入 + 关键词逐字弹出 + 小结卡片延迟入场
- ✅ AnalyzingPage 墨水动画:下落 → 扩散 → 渐隐的三阶段循环
- ✅ 设计模式总结:4 种入场模式 + 命名规范 + 检查清单
下一篇将讲解页面转场动画,探讨 PageTransition 的滑动过渡效果。
📌 收藏提示 :如果您觉得本系列对您有帮助,欢迎点赞 + 关注,也欢迎在评论区交流鸿蒙动画开发的问题!
相关资源: