鸿蒙 ArkTS 动画实战:scale + opacity 组合入场动画深度解析

一、前言

1.1 关于 HarmonyOS NEXT

HarmonyOS NEXT(API 24)是华为自主研发的全场景操作系统,从内核到框架完全去除了 AOSP 代码,实现了全栈自研。其应用开发语言 ArkTS 基于 TypeScript 深度定制,引入了声明式 UI 范式、响应式状态管理以及原生动画引擎。

在 HarmonyOS NEXT 中,动画是用户体验的核心组成部分。ArkTS 官方提供了三层动画体系:

层次 机制 适用场景
属性动画 .animation() 响应状态变化的平滑过渡
转场动画 .transition() 组件出现/消失时的入场离场
显式动画 animateTo() 精细控制的程序化动画

本文聚焦于 转场动画 中一个经典组合:缩放(scale)+ 透明度(opacity)组合入场动画,结合完整代码示例,深入分析其实现原理与技术细节。

1.2 为什么选择 scale + opacity 组合

单一的缩放或透明度变化往往显得单调,而将两者组合使用,可以产生"从虚到实、从小到大的渐进式展开"效果,视觉上更自然、更有层次感。

动画方式 效果描述 适用场景
仅 opacity 单纯淡入,缺少空间感受 文字、图标等简单元素
仅 scale 单纯放大,缺乏柔和感 强调型元素、弹窗
scale + opacity 同时缩放和淡入,观感最自然 列表项、卡片、整体页面内容

这种组合动画在现代移动操作系统中无处不在——iOS 的 App 启动转场、Android 的 Activity 过渡动画,都采用了类似的策略。


二、项目结构与搭建

2.1 工程概览

示例工程基于 HarmonyOS NEXT(API 24)标准模板创建。核心文件结构如下:

entry/src/main/ets/pages/
├── Index.ets              // 首页导航入口
└── ScaleOpacityDemo.ets   // 主演示页面(核心)
entry/src/main/resources/base/profile/
└── main_pages.json        // 页面路由注册

2.2 页面路由配置

main_pages.json 是路由注册文件,任何通过 router.pushUrl() 访问的页面都需在此注册:

{
  "src": [
    "pages/Index",
    "pages/ScaleOpacityDemo"
  ]
}

2.3 首页设计首页使用自定义子组件 DemoEntryCard,并通过 @Prop 装饰器传入属性。这里有一个 ArkTS 命名规范需要特别注意:

onClick 是内置保留属性名,不可用作自定义 @Prop。解决方案是将自定义回调命名为 entryClickbtnClick 等非保留字名称,在组件内部再绑定到 .onClick()

DemoEntryCard({
  title: 'scale + opacity 组合入场动画',
  desc: '缩放 + 透明度同时变化的入场过渡效果',
  icon: '🔍',
  entryClick: () => {
    router.pushUrl({ url: 'pages/ScaleOpacityDemo' });
  }
})

三、核心动画实现:scale + opacity 组合入场

3.1 组件层次结构

ScaleOpacityDemo.ets 共 330 行,组件层级如下:

ScaleOpacityDemo(页面入口,@Entry @Component)
├── TitleBar(顶部标题栏)
├── Column(卡片容器,带 transition 入场)
│   ├── CardView(卡片 1:春日樱花,延迟 0ms)
│   ├── CardView(卡片 2:夏日海浪,延迟 100ms)
│   ├── CardView(卡片 3:秋日红叶,延迟 200ms)
│   └── CardView(卡片 4:冬日白雪,延迟 300ms)
└── ReplayButton(底部重播按钮)

设计意图:外层控制整体淡入放大,内层控制每个卡片逐个显现,两层叠加形成更丰富的视觉层次。

3.2 数据模型

CardItem 使用 @Observed 装饰器使其成为可观察对象:

@Observed
class CardItem {
  id: number;
  title: string;
  desc: string;
  delayIndex: number; // 动画延迟系数,0~3 递增
  // ...
}

delayIndex 是节奏控制参数,最终延迟为 delayIndex * 100ms,产生错落有致的波浪式入场效果。

3.3 父容器入场动画

Column() {
  // 卡片列表
}
.width('100%')
.transition({
  type: TransitionType.Insert,  // 入场动画
  scale: { x: 0, y: 0 },       // 从零放大
  opacity: 0                    // 从透明到不透明
})

效果:组件从"完全透明的一个小点"逐渐变为"不透明的完整大小"。

3.4 子卡片入场动画

每张卡片 CardView 配置了独立的 .transition().animation()

Row() {
  // 卡片内容
}
.transition({
  type: TransitionType.Insert,
  scale: { x: 0.8, y: 0.8 },   // 从 80% 放大到 100%(比 0→1 更轻量)
  opacity: 0
})
.animation({
  duration: this.duration,       // 600ms
  curve: Curve.Friction,         // 摩擦曲线(先快后慢)
  delay: this.delay,             // 错落延迟(0/100/200/300ms)
  playMode: PlayMode.Normal,
  expectedFrameRateRange: {
    min: 60, max: 120, expected: 120  // 利用高刷屏
  }
})

设计思路

  • scale 差异:父容器 scale: 0(从无到有)制造强烈出现感,子卡片 scale: 0.8(轻量放大)提供平滑感
  • 曲线选择Curve.Friction 模拟物理摩擦力,起始快、末尾慢,比 EaseOut 更有"物体冲入画面"的质感
  • 高刷支持expectedFrameRateRange 在 120Hz 设备上以 120fps 渲染,动画丝般顺滑

3.5 双层动画叠加效果

层次 scale 区间 opacity 区间 效果
父层(Column) 0 → 1 0 → 1 整体从一点逐渐展开
子层(CardView) 0.8 → 1 0 → 1 每张卡片依次淡入并放大

两层的叠加效果类似电影"推镜头"——整体画面拉近的同时每个细节逐个浮现,用户体验从生硬的出现变为精心编排的"入场秀"。


四、transition 与 animation 的核心区别

理解这两个 API 的区别是写出正确动画的前提。

4.1 .transition() — 组件级转场

定义组件出现在组件树中(Insert)或移除时(Delete) 的动画效果,由状态变量驱动。

.transition({
  type: TransitionType.Insert,   // Insert / Delete / Appearance / Disappear
  scale: { x: 0, y: 0 },
  opacity: 0
})

4.2 .animation() — 属性值过渡

定义组件的可动画属性变化时的平滑过渡过程。

.animation({
  duration: 600,           // 时长(毫秒)
  curve: Curve.Friction,   // 曲线
  delay: 100               // 延迟
})

4.3 协作用模式

  • transition → 定义"起点"(从什么状态开始)
  • animation → 定义"过程"(如何过渡到终点)

通俗比喻:transition 是导演喊 Action 时演员的起始姿势,animation 是摄影机的拍摄参数。没有 animation,transition 使用默认 300ms linear,机械而缺乏质感。


五、动画重播机制

transition 只在组件首次插入时触发一次,如何让它多次触发?

5.1 核心思路:组件重建

replayAnimation(): void {
  this.showCards = false;       // 隐藏卡片
  setTimeout(() => {
    this.showCards = true;      // 重新显示
    this.playCount++;           // 变更 key,强制重建
  }, 50);
}

5.2 ForEach key 策略

ForEach(this.cardList, (item: CardItem) => {
  CardView({
    card: item,
    cardKey: `card_${item.id}_${this.playCount}`  // playCount 变化 → 组件重建
  })
}, (item: CardItem) => `${item.id}`)

ForEach 的 key 生成器返回 item.id 是稳定标识,而 cardKey prop 是动画触发标识。两条链路互不干扰。

5.3 重播机制完整性

所有卡片动画播放完毕后都处于 opacity: 1, scale: 1,重播时先消失再出现,让用户完整感受入场效果。


六、状态管理的深度应用

6.1 @State 与 @Prop 配合

主组件使用 @State 管理状态:

@State cardList: CardItem[] = [];
@State showCards: boolean = true;
@State playCount: number = 0;

子组件使用 @Prop 接收数据。@State 拥有状态并驱动子组件刷新,@Prop 单向传递只读数据。

6.2 @Observed 的必要性

@Observed 使框架深度观察类实例的属性变化。@State 只能观察到数组引用级别的变化,无法感知 CardItem 内部属性的修改。@Observed 补足了这一能力。

6.3 声明式 Vs 命令式

声明式(ArkTS):
  开发者:当 showCards 为 true 时显示卡片,从 scale=0, opacity=0 开始
  框架:自动执行入场动画

命令式(传统 Android):
  开发者:调用 animate().scaleX(1).alpha(1).setDuration(600).start()
  开发者:手动管理生命周期

声明式动画降低了心智负担,开发者只需关注"什么条件下 UI 应该是什么状态"。


七、动画曲线与性能优化

7.1 Curve 曲线选择

曲线 行为 适用场景
Ease 慢→快→慢 通用 UI 过渡
EaseIn 慢→快 离场动画
EaseOut 快→慢 入场动画
Friction 先快后慢(摩擦力减速) 卡片入场、列表插入
SpringMotion 弹簧弹性 强调型交互

FrictionEaseOut 起始加速度更快,因"摩擦力"迅速减速,产生"冲入画面"的效果。

7.2 帧率策略优化

expectedFrameRateRange: {
  min: 60, max: 120, expected: 120
}

框架参考 expected 设定渲染目标帧率,保证 min 前提下向 expected 靠拢。建议:

  • 高优先级动画 → expected: 120
  • 后台动画 → 使用默认值
  • 大量元素重绘 → 适当降低期望保证稳定性

7.3 性能注意

scale 和 opacity 属于合成器属性,由 GPU 直接处理,不会触发重绘(Repaint)或重排(Reflow),比修改 width/height 高效得多。HarmonyOS NEXT 会自动提交给 GPU 处理。


八、常见错误与避坑

8.1 属性名冲突

错误: onClickkey 是内置保留属性,不能用作 @Prop
解决: 改用 entryClickbtnClickcardKey

8.2 Stack 无 justifyContent

错误: Stack 组件不支持 justifyContent
解决:Column/Row 替代,或使用 StackalignContent + alignment

8.3 不存在的 Curve 枚举

错误: Curve.SpringMotion 在部分 API 版本不可用
解决: 使用该版本明确支持的曲线值,如 Curve.FrictionCurve.FastOutLinearIn

8.4 动画不触发

transition 是一次性的。如果需要重播,必须通过 组件销毁 + 重建(使用 key 变更)或 animateTo 显式动画

@State scaleValue: number = 0;
@State opacityValue: number = 0;

playAnimation(): void {
  animateTo({ duration: 600, curve: Curve.Friction }, () => {
    this.scaleValue = 1;
    this.opacityValue = 1;
  });
}

九、扩展思考:从演示到生产

9.1 通用模板

@Component
struct AnimatedList<T> {
  @Prop items: T[];
  ForEach(items, item => {
    // 每个 item 使用独立 transition + delay
  })
  // 外层 transition
  .transition({ type: TransitionType.Insert, scale: { x: 0.9, y: 0.9 }, opacity: 0 })
}

9.2 更多变换维度

维度 属性 效果
缩放 scale: {x, y, z} 从一定比例开始
透明度 opacity 淡入
位移 translate: {x, y, z} 从偏移位置移入
旋转 rotate: {x, y, z, angle} 旋转入场
模糊 blur 从模糊到清晰

9.3 Accessibility

生产环境应尊重用户的"减少动画"偏好设置:

if (systemSettings.isReduceMotionEnabled) {
  // 跳过动画,直接显示最终状态
} else {
  // 正常执行入场动画
}

十、总结

本文从零到一构建了完整的鸿蒙 ArkTS 入场动画示例,覆盖以下技术点:

  1. @Entry @Component 声明式页面构建
  2. transition API 控制组件出现/消失的转场动画
  3. animation API 控制时长、曲线、延迟和帧率策略
  4. scale + opacity 组合 缩放与透明度同时变化,视觉最自然
  5. 双层 transition 架构 外层集体入场 + 内层逐个显现,形成波浪效果
  6. 组件重建重播机制 状态变量 toggle + key 变更,多次触发 transition
  7. @Observed + @State + @Prop 响应式状态管理体系

文件清单

文件 行数 职责
Index.ets 92 首页导航
ScaleOpacityDemo.ets 330 核心演示
main_pages.json 6 路由注册

下一步方向

  1. 共享元素转场 — 页面间同一元素的平滑过渡
  2. 粒子动画 — Canvas 实现复杂粒子效果
  3. 物理引擎动画 — 碰撞、重力等物理模拟
  4. 自定义曲线CubicBezier 创建专属缓动函数
  5. 动画状态机 — 多动画序列编排

本文基于 HarmonyOS NEXT API 24(SDK 6.1.0.23)构建,在 DevEco Studio 中打开项目可直接编译运行。完整代码见 D:\hongmeng\app612
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

写作日期:2025年7月

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐