鸿蒙新特性:TransitionEffect 转场动画实战 — 构建组件进出动画系统
引言
想象一个场景:用户在列表中添加了一条新数据,这条数据"啪"地一下直接出现——没有过渡,没有动画,就像网页上突然多了一个 DOM 节点。对于开发者来说,这是最高效的实现;但对于用户来说,这很突兀。人眼天然会被运动所吸引,一个平滑的进场动画能让用户直观地感受到"有东西新增了",而不需要额外的提示。
HarmonyOS ArkUI 提供了 TransitionEffect API 来解这个问题。它让你能够为组件的"出现"和"消失"分别定义动画效果——淡入淡出、滑动移入、缩放弹入、旋转进入,甚至进场和退场使用完全不同的动画。这些效果通过声明式 API 调用即可实现,不需要手动管理动画状态。
本文将构建一个"转场动画实验室",深入讲解 TransitionEffect 的核心用法:内置效果(OPACITY、SLIDE、SLIDE_SWITCH)、自定义效果(move、scale、rotate、translate)、以及 combine 组合和 asymmetric 非对称动画。
读完本文你将能够:
- 使用
TransitionEffect.OPACITY/SLIDE/SLIDE_SWITCH等内置转场效果 - 使用
TransitionEffect.move()/scale()/rotate()构建自定义转场 - 使用
.combine()将多个效果组合为一个复合动画 - 使用
.asymmetric()为进场和退场定义不同的动画效果 - 理解
.transition()与animateTo的区别和使用场景
TransitionEffect 概述
什么是 TransitionEffect
TransitionEffect 是 ArkUI 中专用于组件挂载/卸载动画的 API。它与 animateTo 有一个本质区别:
animateTo操作的是已存在组件的属性变化——比如一个 Text 从左边移到右边,Text 本身始终在组件树中。TransitionEffect操作的是组件出现/消失的过程——当组件通过条件渲染(if)或列表变化(ForEach)进入或离开组件树时触发。
换句话说,animateTo 回答"组件从状态 A 到状态 B 怎么过渡",而 TransitionEffect 回答"组件从不存在到存在(或从存在到不存在)怎么过渡"。
触发条件
TransitionEffect 在以下场景触发:
- 条件渲染:
if (condition) { Component().transition(effect) }—— condition 从 false 变为 true 时播放进场动画,从 true 变为 false 时播放退场动画。 - ForEach 增删:列表数据变化导致子项新增或移除时,该子项的 transition 自动触发。
- Navigation 页面转场:通过
pageTransition定义页面级别的进出动画(与组件级 transition 是不同 API)。
API 形态
TransitionEffect 是一个不可变类,所有方法返回新的 TransitionEffect 实例,支持链式调用:
// 基础形式
.transition(TransitionEffect.OPACITY)
// 自定义动画参数
.transition(TransitionEffect.OPACITY.animation({ duration: 400, curve: Curve.EaseOut }))
// 组合多个效果
.transition(
TransitionEffect.scale({ x: 0, y: 0 })
.combine(TransitionEffect.OPACITY)
.animation({ duration: 450, curve: Curve.EaseOut })
)
// 进场和退场使用不同效果
.transition(TransitionEffect.asymmetric(
// 进场效果
TransitionEffect.move(TransitionEdge.START).combine(TransitionEffect.OPACITY)
.animation({ duration: 500, curve: Curve.EaseOut }),
// 退场效果
TransitionEffect.scale({ x: 0.5, y: 0.5 }).combine(TransitionEffect.OPACITY)
.animation({ duration: 400, curve: Curve.EaseIn })
))
核心 API 逐项解析
内置转场效果
ArkUI 提供了三种开箱即用的转场效果,它们的效果是预定义的,你只需要调用即可:
TransitionEffect.OPACITY
透明度转场。进场时组件从完全透明(opacity=0)渐变到完全不透明(opacity=1),退场时反向操作。
.transition(TransitionEffect.OPACITY.animation({ duration: 400, curve: Curve.EaseOut }))
这是最常用的转场效果,也是性能开销最小的——它只操作 opacity 属性,不涉及任何布局或变换计算。适合列表项的增删、通知的弹出等场景。
TransitionEffect.SLIDE
滑动转场。进场时组件从左侧滑入到正常位置,退场时从正常位置向右滑出。
.transition(TransitionEffect.SLIDE.animation({ duration: 400, curve: Curve.EaseOut }))
SLIDE 的滑动方向是固定的(左→右进场,右→左退场),适合水平方向的列表操作。
TransitionEffect.SLIDE_SWITCH
滑动切换转场。与 SLIDE 类似,但额外叠加了一个轻微的缩放效果——新元素滑入时带有从 0.8→1.0 的缩放,被替换的元素滑出时带有从 1.0→0.8 的缩放。默认时长为 600ms。
.transition(TransitionEffect.SLIDE_SWITCH.animation({ duration: 500, curve: Curve.EaseOut }))
这个效果适合 Tab 切换、内容替换等场景,比纯 SLIDE 多了一层视觉层次感。
自定义转场效果
当内置效果不满足需求时,可以使用静态方法构建自定义转场:
TransitionEffect.move(edge: TransitionEdge)
从指定边缘移入/移出。TransitionEdge 有四个值:TOP、BOTTOM、START、END。其中 START 和 END 会根据布局方向自动适配(LTR 语言中 START=LEFT,RTL 语言中 START=RIGHT)。
// 从顶部移入
.transition(TransitionEffect.move(TransitionEdge.TOP)
.animation({ duration: 400, curve: Curve.EaseOut }))
// 从底部移入
.transition(TransitionEffect.move(TransitionEdge.BOTTOM)
.animation({ duration: 400, curve: Curve.EaseOut }))
进场时组件从 edge 指定的方向移入到正常位置,退场时组件从正常位置向 edge 方向移出。注意:move 的效果取决于 edge 参数,与组件自身的位置无关——即使组件在屏幕底部,move(TOP) 仍然会让它从顶部外移入。
TransitionEffect.scale(options)
缩放转场。参数 { x?: number, y?: number } 指定缩放的中心点(相对于组件自身的锚点),取值范围 0~1。
// 从 0 放大到 1(弹出效果)
.transition(TransitionEffect.scale({ x: 0, y: 0 })
.animation({ duration: 450, curve: Curve.EaseOut }))
// 从 0.5 放大到 1(轻微弹出)
.transition(TransitionEffect.scale({ x: 0.5, y: 0.5 })
.animation({ duration: 400, curve: Curve.EaseIn }))
scale({ x: 0, y: 0 }) 是最激进的缩放效果——组件从不可见的点放大到正常大小,类似"弹出"或"放大出现"。scale({ x: 0.5, y: 0.5 }) 则更温和。
TransitionEffect.rotate(options)
旋转转场。参数 { x?: number, y?: number, z?: number, angle: number } 指定旋转轴和角度:
z: 1, angle: 180— 绕 Z 轴(垂直于屏幕)旋转 180°,即平面内旋转x: 1, angle: 90— 绕 X 轴(水平轴)旋转 90°,即翻转效果y: 1, angle: 90— 绕 Y 轴(垂直轴)旋转 90°,即侧翻效果
// 绕 Z 轴旋转 180° + 同时淡入
.transition(
TransitionEffect.rotate({ z: 1, angle: 180 })
.animation({ duration: 500, curve: Curve.EaseOut })
.combine(TransitionEffect.OPACITY.animation({ duration: 300 }))
)
单独的 rotate 效果通常不够自然——一个旋转进入但完全不透明的组件会显得突兀。因此 rotate 通常与 OPACITY 组合使用(用 .combine()),让组件在旋转的同时逐渐显现。
TransitionEffect.translate(options)
平移转场。参数 { x?: number, y?: number } 指定平移的起始偏移量(相对于组件正常位置)。与 move 不同,translate 让你可以精确控制偏移的方向和距离。
// 从右上方 100vp 处斜向移入
.transition(
TransitionEffect.translate({ x: 100, y: -50 })
.animation({ duration: 400, curve: Curve.EaseOut })
.combine(TransitionEffect.OPACITY)
)
translate 给了开发者最大的自由度——你可以实现任何方向的平移,也可以实现对角线方向的移动。
组合效果:combine()
.combine() 方法将两个 TransitionEffect 合并为一个——两个效果同时播放,形成复合动画。
TransitionEffect.scale({ x: 0, y: 0 }) // 缩放
.combine(TransitionEffect.OPACITY) // + 淡入
.animation({ duration: 450, curve: Curve.EaseOut })
这个例子中,组件同时在缩放和淡入——从 0 大小的透明状态,在 450ms 内变为正常大小的不透明状态。
combine 的调用顺序不影响动画效果(A.combine(B) 和 B.combine(A) 效果相同),但会影响动画参数的继承:.animation() 只作用于它之前链式调用的效果。如果你需要两个效果有不同的 duration,可以为每个效果分别设置 animation,然后用 combine 合并:
TransitionEffect.rotate({ z: 1, angle: 180 })
.animation({ duration: 500, curve: Curve.EaseOut })
.combine(
TransitionEffect.OPACITY.animation({ duration: 300 })
)
这里旋转耗时 500ms,而淡入只需 300ms——淡入先完成,旋转还在继续,创造了"旋转着浮现"的效果。
非对称动画:asymmetric()
.asymmetric(appearEffect, disappearEffect) 是 TransitionEffect 最强大的特性之一——它允许进场和退场使用完全不同的动画。
.transition(TransitionEffect.asymmetric(
// 进场:从左侧滑入 + 淡入
TransitionEffect.move(TransitionEdge.START)
.combine(TransitionEffect.OPACITY)
.animation({ duration: 500, curve: Curve.EaseOut }),
// 退场:缩小到 50% + 淡出
TransitionEffect.scale({ x: 0.5, y: 0.5 })
.combine(TransitionEffect.OPACITY)
.animation({ duration: 400, curve: Curve.EaseIn })
))
这种非对称设计符合用户的直觉:新元素从某个方向"进来",旧元素以不同的方式"离开"。常见的搭配模式有:
| 进场 | 退场 | 适用场景 |
|---|---|---|
| move(START) + OPACITY | scale(0.5) + OPACITY | 列表增删 |
| move(BOTTOM) + OPACITY | OPACITY | 通知弹出 |
| scale(0) + OPACITY | scale(0) + OPACITY | 弹窗/对话框 |
| rotate + OPACITY | move(END) + OPACITY | 页面切换 |
animation() 参数
.animation({ duration, curve, delay }) 为转场效果指定动画参数:
duration: number— 动画持续时间,单位毫秒。推荐范围 300-600ms,太短用户感知不到,太长用户会不耐烦。curve: Curve— 动画曲线,控制动画的速率变化。Curve.EaseOut(快进慢出)适合进场动画,Curve.EaseIn(慢进快出)适合退场动画,Curve.EaseInOut适合对称场景。delay: number— 延迟时间,单位毫秒。用于错开多个动画的播放时间。

Demo 设计:转场动画实验室
本文 Demo 实现了一个完整的转场动画实验室,包含以下功能:
页面结构
Column(根容器)
├── Header(深色标题栏:"转场动画实验室" + TransitionEffect 标签)
├── 效果选择器(8 个效果按钮 + 效果描述文字)
├── 动画卡片列表(Scroll + ForEach,每条卡片应用转场效果)
└── 控制按钮栏(添加卡片 / 移除最后 / 清空全部)
8 种转场效果
Demo 提供了 8 种转场效果的实时切换:
| 序号 | 效果名称 | 按钮文字 | 实现方式 |
|---|---|---|---|
| 1 | 淡入淡出 | 淡入淡出 | TransitionEffect.OPACITY |
| 2 | 水平滑动 | 水平滑动 | TransitionEffect.SLIDE |
| 3 | 滑入缩放 | 滑入缩放 | TransitionEffect.SLIDE_SWITCH |
| 4 | 上方移入 | 上方移入 | TransitionEffect.move(TOP) |
| 5 | 下方移入 | 下方移入 | TransitionEffect.move(BOTTOM) |
| 6 | 缩放弹入 | 缩放弹入 | TransitionEffect.scale({ x: 0, y: 0 }) |
| 7 | 旋转进入 | 旋转进入 | TransitionEffect.rotate({ z: 1, angle: 180 }) + OPACITY |
| 8 | 组合效果 | 组合效果 | asymmetric(move+OPACITY, scale+OPACITY) |
选择不同效果后,后续添加或移除的卡片将使用选中的转场动画。已存在的卡片不受影响。
4 个交互点
- 切换转场效果:点击效果标签按钮,实时切换当前使用的转场类型,下方描述文字同步更新。
- 添加卡片:点击"添加卡片"按钮,一个带颜色的卡片通过选中的转场效果进入列表。
- 移除最后一张:点击"移除最后"按钮,最后一张卡片通过选中的转场效果离开列表。
- 清空全部:点击"清空全部"按钮,所有卡片一次性离开列表,每张卡片独立播放退场动画。
核心实现
数据模型
class AnimatedItem {
id: number;
label: string;
color: string;
constructor(id: number, label: string, color: string) {
this.id = id;
this.label = label;
this.color = color;
}
}
class EffectOption {
name: string;
key: string;
constructor(name: string, key: string) {
this.name = name;
this.key = key;
}
}
两个数据类分别用于卡片数据和效果选项。EffectOption 的 key 字段用于标识当前选中的效果类型。
效果选择器实现
private effectOptions: EffectOption[] = [
new EffectOption('淡入淡出', 'opacity'),
new EffectOption('水平滑动', 'slide'),
new EffectOption('滑入缩放', 'slideSwitch'),
new EffectOption('上方移入', 'moveTop'),
new EffectOption('下方移入', 'moveBottom'),
new EffectOption('缩放弹入', 'scale'),
new EffectOption('旋转进入', 'rotate'),
new EffectOption('组合效果', 'combined'),
];
效果选择器通过 Flex({ wrap: FlexWrap.Wrap }) 实现流式布局,选中的效果按钮使用蓝色高亮:
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.effectOptions, (opt: EffectOption) => {
Text(opt.name)
.fontSize(12)
.fontColor(this.selectedEffect === opt.key ? '#FFFFFF' : '#444455')
.fontWeight(this.selectedEffect === opt.key ? FontWeight.Medium : FontWeight.Normal)
.padding({ top: 7, bottom: 7, left: 14, right: 14 })
.borderRadius(8)
.backgroundColor(this.selectedEffect === opt.key ? '#1677FF' : '#F5F5F5')
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.selectedEffect = opt.key; })
}, (opt: EffectOption) => opt.key)
}
.width('100%')
getTransitionEffect() 核心方法
getTransitionEffect(): TransitionEffect {
if (this.selectedEffect === 'opacity') {
return TransitionEffect.OPACITY.animation({ duration: 400, curve: Curve.EaseOut });
}
if (this.selectedEffect === 'slide') {
return TransitionEffect.SLIDE.animation({ duration: 400, curve: Curve.EaseOut });
}
if (this.selectedEffect === 'slideSwitch') {
return TransitionEffect.SLIDE_SWITCH.animation({ duration: 500, curve: Curve.EaseOut });
}
if (this.selectedEffect === 'moveTop') {
return TransitionEffect.move(TransitionEdge.TOP)
.animation({ duration: 400, curve: Curve.EaseOut });
}
if (this.selectedEffect === 'moveBottom') {
return TransitionEffect.move(TransitionEdge.BOTTOM)
.animation({ duration: 400, curve: Curve.EaseOut });
}
if (this.selectedEffect === 'scale') {
return TransitionEffect.scale({ x: 0, y: 0 })
.animation({ duration: 450, curve: Curve.EaseOut });
}
if (this.selectedEffect === 'rotate') {
return TransitionEffect.rotate({ z: 1, angle: 180 })
.animation({ duration: 500, curve: Curve.EaseOut })
.combine(TransitionEffect.OPACITY.animation({ duration: 300 }));
}
if (this.selectedEffect === 'combined') {
return TransitionEffect.asymmetric(
TransitionEffect.move(TransitionEdge.START)
.combine(TransitionEffect.OPACITY)
.animation({ duration: 500, curve: Curve.EaseOut }),
TransitionEffect.scale({ x: 0.5, y: 0.5 })
.combine(TransitionEffect.OPACITY)
.animation({ duration: 400, curve: Curve.EaseIn })
);
}
return TransitionEffect.OPACITY;
}
这个方法是 Demo 的核心——它根据 selectedEffect 的当前值,返回对应的 TransitionEffect 对象。每个卡片在模板中都调用 .transition(this.getTransitionEffect()),因此切换效果类型后,新添加或移除的卡片就会使用新的动画效果。
值得注意的是 rotate 效果:它使用了 .combine(TransitionEffect.OPACITY.animation({ duration: 300 }))。因为如果只有旋转而没有透明度变化,组件在旋转的前半段(旋转角 > 90° 时)会显得很奇怪——你看到的是一个正在旋转的完全不透明的卡片。叠加上淡入效果后,卡片在旋转的同时逐渐显现,视觉上自然得多。
卡片列表与转场绑定
if (this.items.length > 0) {
Scroll() {
Column() {
ForEach(this.items, (item: AnimatedItem) => {
Row() {
Text(item.label)
.fontSize(15)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding(18)
.borderRadius(10)
.backgroundColor(item.color)
.margin({ bottom: 10 })
.transition(this.getTransitionEffect())
}, (item: AnimatedItem) => item.id.toString())
}
.width('100%')
.padding({ left: Spacing.LG, right: Spacing.LG, top: 14 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
}
关键点:.transition() 绑定到了 ForEach 内部的每个 Row() 上。当 this.items 数组变化时——新增元素触发进场动画,移除元素触发退场动画。ArkUI 框架自动处理动画的播放和清理,不需要开发者手动管理。
状态更新模式
在 ArkTS 严格模式下,数组的增删必须通过不可变更新模式——先 slice() 创建副本,修改副本,再赋值回去:
addItem(): void {
let colorIndex: number = this.nextId % this.colors.length;
let newItems: AnimatedItem[] = this.items.slice();
newItems.push(new AnimatedItem(this.nextId, '卡片 ' + this.nextId.toString(), this.colors[colorIndex]));
this.items = newItems;
this.nextId = this.nextId + 1;
}
removeLast(): void {
if (this.items.length > 0) {
let newItems: AnimatedItem[] = this.items.slice();
newItems.pop();
this.items = newItems;
}
}
clearAll(): void {
this.items = [];
this.nextId = 1;
}
this.items = newItems 触发了 @State 的变化检测,ArkUI 比较新旧数组,确定哪些元素是新增的(播放进场动画),哪些被移除了(播放退场动画)。
动画时机与性能考量
TransitionEffect 与 animateTo 的选择
很多开发者在初次接触 ArkUI 动画时会困惑:什么时候用 TransitionEffect,什么时候用 animateTo?
一个简化的判断标准:
| 场景 | 使用 API | 原因 |
|---|---|---|
| 组件出现/消失 | TransitionEffect | 专为此场景设计,自动处理挂载/卸载时序 |
| 组件属性变化(位置、大小、颜色等) | animateTo | 操作已存在组件的属性过渡 |
| 连续动画序列 | animateTo + onFinish | 支持链式调用和回调 |
| 手势跟随 | animateTo | 支持实时更新目标值 |
TransitionEffect 的核心价值在于自动化——你不需要在添加数据前设置初始状态,也不需要担心动画完成后组件是否应该存在于组件树中。框架替你处理了这些细节。
性能注意事项
-
避免过多的 combine:虽然
combine()在语义上很清晰,但每个 combine 都对应一个独立的动画引擎轨道。超过 3 个 combine 的效果组合可能在某些低端设备上导致掉帧。实际上,1-2 个效果(如 move + OPACITY)已经足够表达绝大多数转场意图。 -
duration 不宜过长:转场动画的目的是让用户感知到变化,而不是展示动画本身。推荐 duration 在 300-500ms 之间。超过 600ms 的转场动画会让用户觉得"界面反应慢"。
-
列表批量操作:当清空一个包含大量元素的列表时,每个元素都会独立播放退场动画。如果元素数量超过 20-30 个,建议缩短退场动画的 duration(200-300ms),避免总退场时间过长。
-
OPACITY 是最经济的:在所有转场效果中,OPACITY 的性能开销最小,因为它不涉及布局重计算或变换矩阵运算。在性能敏感的滚动列表中,优先使用 OPACITY。
TransitionEffect 与页面转场
组件级 TransitionEffect(本文讨论的 .transition())与页面级转场(pageTransition)是两个不同层面的 API:
// 组件级转场 — 组件在页面内出现/消失
Component()
.transition(TransitionEffect.OPACITY)
// 页面级转场 — 整个页面在导航中出现/消失
@Entry
@Component
struct MyPage {
pageTransition() {
PageTransitionEnter({ duration: 300 })
.slide(SlideEffect.Right)
}
}
两者的适用场景不同:组件级转场用于页面内部的元素动画(如列表增删、弹窗出现),页面级转场用于 Navigation 导航时的页面切换动画。一个常见的做法是两者配合使用——页面用 slide 转场进入,页面内的元素用 OPACITY 转场逐项出现。
总结
本文通过构建一个"转场动画实验室",深入讲解了 HarmonyOS ArkUI 中 TransitionEffect 组件的核心用法:
- 三种内置效果:
OPACITY(透明度)、SLIDE(滑动)、SLIDE_SWITCH(滑动+缩放),提供开箱即用的常见转场。 - 四种自定义效果:
move(edge)(指定方向移入)、scale({x, y})(缩放)、rotate({x, y, z, angle})(旋转)、translate({x, y})(偏移),支持构建任意转场效果。 - combine() 组合:将多个转场效果合并为一个复合动画,所有效果同时播放。
- asymmetric() 非对称:进场和退场使用完全不同的动画效果,让"进来"和"离开"各有各的视觉语言。
- animation() 参数:duration、curve、delay 三个参数控制动画的时长、速率曲线和延迟。
TransitionEffect 是 ArkUI 声明式动画体系的重要组成部分。它与 animateTo 形成了互补——animateTo 处理"已存在组件"的属性动画,TransitionEffect 处理"组件进入/离开"的生命周期动画。两者配合使用,可以构建出流畅、自然、有层次的界面动画系统。
动画不是装饰——它是界面语言的一部分。一个好的转场动画比一行文字说明更能让用户直观理解"这个卡片是新添加的"、“那个通知已经消失了”。用户可能不会意识到动画的存在,但如果缺少它,用户一定会觉得界面有些"生硬"。这就是 TransitionEffect 的价值所在:它让界面的状态变化变得可感知。
更多推荐



所有评论(0)