【共创季稿事节】鸿蒙 ArkTS 动画实战
鸿蒙 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。解决方案是将自定义回调命名为entryClick、btnClick等非保留字名称,在组件内部再绑定到.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 | 弹簧弹性 | 强调型交互 |
Friction 比 EaseOut 起始加速度更快,因"摩擦力"迅速减速,产生"冲入画面"的效果。
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 属性名冲突
错误: onClick、key 是内置保留属性,不能用作 @Prop
解决: 改用 entryClick、btnClick、cardKey 等
8.2 Stack 无 justifyContent
错误: Stack 组件不支持 justifyContent
解决: 用 Column/Row 替代,或使用 Stack 的 alignContent + alignment
8.3 不存在的 Curve 枚举
错误: Curve.SpringMotion 在部分 API 版本不可用
解决: 使用该版本明确支持的曲线值,如 Curve.Friction、Curve.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 入场动画示例,覆盖以下技术点:
@Entry @Component声明式页面构建transitionAPI 控制组件出现/消失的转场动画animationAPI 控制时长、曲线、延迟和帧率策略- scale + opacity 组合 缩放与透明度同时变化,视觉最自然
- 双层 transition 架构 外层集体入场 + 内层逐个显现,形成波浪效果
- 组件重建重播机制 状态变量 toggle + key 变更,多次触发 transition
@Observed+@State+@Prop响应式状态管理体系
文件清单
| 文件 | 行数 | 职责 |
|---|---|---|
Index.ets |
92 | 首页导航 |
ScaleOpacityDemo.ets |
330 | 核心演示 |
main_pages.json |
6 | 路由注册 |
下一步方向
- 共享元素转场 — 页面间同一元素的平滑过渡
- 粒子动画 — Canvas 实现复杂粒子效果
- 物理引擎动画 — 碰撞、重力等物理模拟
- 自定义曲线 —
CubicBezier创建专属缓动函数 - 动画状态机 — 多动画序列编排
本文基于 HarmonyOS NEXT API 24(SDK 6.1.0.23)构建,在 DevEco Studio 中打开项目可直接编译运行。完整代码见
D:\hongmeng\app612。
写作日期:2025年7月
更多推荐







所有评论(0)