问题描述
如何在 HarmonyOS 中实现流畅且性能优良的动画效果?开发者常遇到:
- 动画卡顿,帧率不稳定
- 不知道用 animateTo 还是 transition
- 列表动画性能差
- 深色模式切换没有过渡效果
关键字:ArkUI 动画 、animateTo 、transition 、性能优化
解决方案
1. 动画核心原理
ArkUI动画系统:
┌─────────────────────────────────┐
│ animateTo (显式动画) │
│ - 通过改变状态触发动画 │
│ - 适用于交互动画 │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ transition (转场动画) │
│ - 组件出现/消失时的动画 │
│ - 适用于列表项、弹窗 │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ animation (属性动画) │
│ - 绑定到特定属性 │
│ - 适用于简单动画 │
└─────────────────────────────────┘
2. 完整实现代码
案例 1: 卡片点击缩放动画
@Component
struct AnimatedCard {
@State isPressed: boolean = false;
build() {
Column() {
Text('点击我')
.fontSize(16)
.fontColor(Color.White);
}
.width(200)
.height(100)
.backgroundColor('#FF6B3D')
.borderRadius(12)
// ✅ 使用scale实现缩放
.scale(this.isPressed ? { x: 0.95, y: 0.95 } : { x: 1, y: 1 })
// ✅ 绑定动画属性
.animation({
duration: 200,
curve: Curve.EaseOut
})
// ✅ 点击事件
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.isPressed = true;
} else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
this.isPressed = false;
}
})
}
}
效果: 点击时卡片缩小到 95%,松开恢复,有弹性感
案例 2: 列表项出现动画
@Component
struct AnimatedList {
@State items: string[] = ['项目1', '项目2', '项目3'];
build() {
List({ space: 12 }) {
ForEach(this.items, (item: string, index: number) => {
ListItem() {
this.buildListItemContent(item);
}
// ✅ 添加transition实现出现动画
.transition(TransitionEffect.OPACITY
.animation({ duration: 300, delay: index * 100 }) // 错开延迟
.combine(TransitionEffect.translate({ y: 50 }))
)
})
}
.width('100%')
}
@Builder
buildListItemContent(item: string) {
Row() {
Text(item)
.fontSize(16)
.fontColor('#333333');
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
}
// 添加新项目
addItem(): void {
this.items.push(`项目${this.items.length + 1}`);
}
}
效果: 新增列表项从下方淡入滑入,多个项目错开出现
案例 3: 主题切换过渡动画
@Entry
@Component
struct ThemeSwitchDemo {
@State isDark: boolean = false;
build() {
Column({ space: 20 }) {
// 标题栏
Row() {
Text('主题切换演示')
.fontSize(20)
.fontWeight(FontWeight.Bold)
// ✅ 文字颜色动画
.fontColor(this.isDark ? '#F0F0F0' : '#2D1F15')
.animation({
duration: 300,
curve: Curve.EaseInOut
})
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
// ✅ 背景色动画
.backgroundColor(this.isDark ? '#2C2C2C' : '#FFFFFF')
.animation({
duration: 300,
curve: Curve.EaseInOut
})
// 内容区域
Column({ space: 12 }) {
this.buildCard('卡片1');
this.buildCard('卡片2');
this.buildCard('卡片3');
}
.width('100%')
.layoutWeight(1)
.padding(16)
// 切换按钮
Button(this.isDark ? '☀️ 浅色模式' : '🌙 深色模式')
.width('90%')
.height(48)
.fontSize(16)
.backgroundColor('#FF6B3D')
.onClick(() => {
// ✅ 使用animateTo包裹状态变化
animateTo({
duration: 300,
curve: Curve.EaseInOut
}, () => {
this.isDark = !this.isDark;
});
})
}
.width('100%')
.height('100%')
// ✅ 主背景动画
.backgroundColor(this.isDark ? '#1A1A1A' : '#F5F5F5')
.animation({
duration: 300,
curve: Curve.EaseInOut
})
}
@Builder
buildCard(title: string) {
Column() {
Text(title)
.fontSize(16)
.fontColor(this.isDark ? '#F0F0F0' : '#2D1F15')
.animation({
duration: 300,
curve: Curve.EaseInOut
})
}
.width('100%')
.height(80)
.justifyContent(FlexAlign.Center)
.backgroundColor(this.isDark ? '#2C2C2C' : '#FFFFFF')
.borderRadius(12)
.animation({
duration: 300,
curve: Curve.EaseInOut
})
}
}
效果: 点击按钮后,背景、卡片、文字颜色平滑过渡,无闪烁
案例 4: 浮动按钮展开动画
@Component
struct FloatingActionButton {
@State isExpanded: boolean = false;
build() {
Stack({ alignContent: Alignment.BottomEnd }) {
// 展开的子按钮
if (this.isExpanded) {
Column({ space: 16 }) {
this.buildSubButton('📝', '记录', 2);
this.buildSubButton('📊', '统计', 1);
this.buildSubButton('⚙️', '设置', 0);
}
.position({ x: 0, y: -180 })
// ✅ 展开动画
.transition(TransitionEffect.OPACITY
.animation({ duration: 200 })
.combine(TransitionEffect.scale({ x: 0.5, y: 0.5 }))
)
}
// 主按钮
Column() {
Text(this.isExpanded ? '✕' : '+')
.fontSize(32)
.fontColor(Color.White)
// ✅ 图标旋转动画
.rotate({ angle: this.isExpanded ? 45 : 0 })
.animation({
duration: 300,
curve: Curve.EaseInOut
})
}
.width(56)
.height(56)
.justifyContent(FlexAlign.Center)
.backgroundColor('#FF6B3D')
.borderRadius(28)
// ✅ 阴影动画
.shadow({
radius: this.isExpanded ? 12 : 8,
color: 'rgba(255, 107, 61, 0.4)',
offsetY: this.isExpanded ? 6 : 4
})
.animation({
duration: 300,
curve: Curve.EaseOut
})
.onClick(() => {
animateTo({
duration: 300,
curve: Curve.EaseInOut
}, () => {
this.isExpanded = !this.isExpanded;
});
})
}
.width('100%')
.height('100%')
.padding({ right: 24, bottom: 24 })
}
@Builder
buildSubButton(icon: string, label: string, index: number) {
Row({ space: 12 }) {
Text(label)
.fontSize(14)
.fontColor('#333333')
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({
radius: 4,
color: 'rgba(0, 0, 0, 0.1)',
offsetY: 2
})
Column() {
Text(icon).fontSize(24);
}
.width(48)
.height(48)
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(24)
.shadow({
radius: 6,
color: 'rgba(0, 0, 0, 0.15)',
offsetY: 3
})
}
.justifyContent(FlexAlign.End)
// ✅ 子按钮错开出现
.transition(TransitionEffect.OPACITY
.animation({ duration: 200, delay: index * 50 })
.combine(TransitionEffect.translate({ x: 50 }))
)
}
}
效果: 点击主按钮,子按钮从右侧淡入滑入,错开出现
案例 5: 数据加载骨架屏动画
@Component
struct SkeletonLoading {
@State isLoading: boolean = true;
@State shimmerOffset: number = 0;
private timerId: number = -1;
aboutToAppear(): void {
// ✅ 启动闪烁动画
this.startShimmer();
}
aboutToDisappear(): void {
// 清理定时器
if (this.timerId >= 0) {
clearInterval(this.timerId);
}
}
/**
* 启动闪烁动画
*/
private startShimmer(): void {
this.timerId = setInterval(() => {
animateTo({
duration: 1500,
curve: Curve.Linear
}, () => {
this.shimmerOffset = (this.shimmerOffset + 1) % 100;
});
}, 1500);
}
build() {
Column({ space: 16 }) {
ForEach([1, 2, 3], (index: number) => {
this.buildSkeletonCard();
})
}
.width('100%')
.padding(16)
}
@Builder
buildSkeletonCard() {
Column({ space: 12 }) {
// 标题骨架
Row()
.width('60%')
.height(20)
.backgroundColor('#E0E0E0')
.borderRadius(4)
.linearGradient({
angle: 90,
colors: [
[0xE0E0E0, 0.0],
[0xF0F0F0, 0.5],
[0xE0E0E0, 1.0]
]
})
// ✅ 闪烁动画
.animation({
duration: 1500,
iterations: -1,
playMode: PlayMode.Alternate
})
// 内容骨架
Row()
.width('100%')
.height(16)
.backgroundColor('#E0E0E0')
.borderRadius(4)
.linearGradient({
angle: 90,
colors: [
[0xE0E0E0, 0.0],
[0xF0F0F0, 0.5],
[0xE0E0E0, 1.0]
]
})
.animation({
duration: 1500,
iterations: -1,
playMode: PlayMode.Alternate
})
Row()
.width('80%')
.height(16)
.backgroundColor('#E0E0E0')
.borderRadius(4)
.linearGradient({
angle: 90,
colors: [
[0xE0E0E0, 0.0],
[0xF0F0F0, 0.5],
[0xE0E0E0, 1.0]
]
})
.animation({
duration: 1500,
iterations: -1,
playMode: PlayMode.Alternate
})
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
}
}
效果: 加载时显示闪烁的骨架屏,提升用户体验
关键要点
1. 动画性能优化
✅ 优先使用 transform 属性:
// ✅ 性能好 - GPU加速
.scale({ x: 0.95, y: 0.95 })
.translate({ x: 10, y: 10 })
.rotate({ angle: 45 })
.opacity(0.5)
// ❌ 性能差 - 触发重排
.width(200) // 不要用于动画
.height(100) // 不要用于动画
2. 曲线选择
// 常用动画曲线
Curve.EaseOut // 淡出效果,适合出现动画
Curve.EaseIn // 淡入效果,适合消失动画
Curve.EaseInOut // 平滑过渡,适合状态切换
Curve.Linear // 线性,适合循环动画
Curve.Sharp // 锐利,适合点击反馈
3. 避免过度动画
❌ 不要这样做:
// 每个属性都加动画,性能差
.width(this.w).animation({ duration: 300 })
.height(this.h).animation({ duration: 300 })
.backgroundColor(this.bg).animation({ duration: 300 })
.fontColor(this.color).animation({ duration: 300 })
✅ 推荐做法:
// 只对关键属性添加动画
.backgroundColor(this.bg)
.fontColor(this.color)
.animation({ // 统一动画配置
duration: 300,
curve: Curve.EaseInOut
})
4. 列表动画优化
// ✅ 使用cachedCount提升性能
List({ space: 12 }) {
ForEach(this.items, (item: Item) => {
ListItem() {
this.buildListItem(item);
}
.transition(TransitionEffect.OPACITY)
})
}
.cachedCount(5) // 缓存5个列表项
最佳实践
1. animateTo vs animation
animateTo:
-
用于复杂交互动画
-
可以同时控制多个属性
-
适合用户操作触发的动画
Button('点击')
.onClick(() => {
animateTo({ duration: 300 }, () => {
this.size = 100;
this.color = Color.Red;
this.rotation = 45;
});
})
animation:
-
用于简单属性动画
-
绑定到单个组件
-
适合状态变化时自动播放
Text('文字')
.fontSize(this.size)
.animation({ duration: 300 })
2. 组合动画
// ✅ 使用combine组合多个效果
.transition(
TransitionEffect.OPACITY
.animation({ duration: 300 })
.combine(TransitionEffect.translate({ y: 50 }))
.combine(TransitionEffect.scale({ x: 0.8, y: 0.8 }))
)
3. 错开动画
// ✅ 使用delay制造错开效果
ForEach(this.items, (item: Item, index: number) => {
ListItem() {
// ...
}
.transition(TransitionEffect.OPACITY
.animation({
duration: 300,
delay: index * 100 // 每个延迟100ms
})
)
})
常见问题
Q1: 动画卡顿怎么办?
检查:
- 是否改变了 width/height 等布局属性
- 是否在动画中执行复杂计算
- 列表项是否过多
解决:
// ✅ 使用transform代替width/height
.scale({ x: 1.2, y: 1.2 }) // 不触发重排
// ✅ 使用cachedCount
.cachedCount(10)
// ✅ 使用renderGroup
.renderGroup(true)
Q2: 列表动画性能差?
// ✅ 优化方案
List() {
LazyForEach(this.dataSource, (item: Item) => {
ListItem() {
// 简化动画
this.buildListItem(item);
}
// 只在插入/删除时播放动画
.transition(TransitionEffect.OPACITY
.animation({ duration: 200 }) // 缩短时长
)
})
}
.cachedCount(10) // 增加缓存
.renderGroup(true) // 开启渲染组
总结
✅ 性能优先 : 使用 transform 属性 ✅ 合理选择 : animateTo vs animation ✅ 曲线优化 : 根据场景选择曲线 ✅ 组合动画 : combine 制造复杂效果 ✅ 错开播放 : delay 制造层次感 ✅ 列表优化: cachedCount + renderGroup