鸿蒙实战:入场动画设计 — 分段式页面入场

鸿蒙实战:入场动画设计 --- 分段式页面入场

前言(必读)

图:鸿蒙实战:入场动画设计 --- 分段式页面入场(第 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 的滑动过渡效果。


📌 收藏提示 :如果您觉得本系列对您有帮助,欢迎点赞 + 关注,也欢迎在评论区交流鸿蒙动画开发的问题!


相关资源:

相关推荐
水龙吟啸8 小时前
华为2026.6.3机考选择题+编程题【速刷敲黑板】
人工智能·深度学习·算法·华为
爱吃大芒果8 小时前
商用门店应用开发:AbilityKit Kiosk 锁屏模式拆解
网络·华为·harmonyos
爸爸6198 小时前
30 原始 SQL 查询与聚合统计:querySql 实战
数据库·sql·华为·harmonyos·鸿蒙系统
星释17 小时前
鸿蒙智能体开发实战:23.常见问题
服务器·华为·ai·harmonyos·鸿蒙·智能体
子非鱼942717 小时前
HarmonyOS律愈实战09:HarmonyOS Form播放器卡片实现
华为·harmonyos
全堆鸿蒙17 小时前
25 数据库设计与表结构规划:5 张表的设计思路
数据库·华为·cocoa·harmonyos
listening77718 小时前
HarmonyOS 6.1 全场景开发终篇:从单设备到万物互联的实战复盘与架构思考
华为·架构·harmonyos
星释20 小时前
鸿蒙智能体开发实战:39.鸿蒙壁纸大师 - 前端卡片显示与交互优化
前端·华为·交互·harmonyos·鸿蒙·火山引擎
不羁的木木20 小时前
HarmonyOS APP实战-画图APP - 第1篇:环境搭建与项目初始化
华为·harmonyos
tyqtyq221 天前
药品说明书解读 —— AI 安全用药助手,鸿蒙原生应用深度解析
人工智能·学习·华为·生活·harmonyos