问题描述
如何在 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
参考资料
- ArkUI 动画开发指南
- 显式动画 animateTo
- 转场动画 TransitionEffect