[鸿蒙2025领航者闯关] ArkUI动画实战

问题描述

如何在 HarmonyOS 中实现流畅且性能优良的动画效果?开发者常遇到:

  • 动画卡顿,帧率不稳定
  • 不知道用 animateTo 还是 transition
  • 列表动画性能差
  • 深色模式切换没有过渡效果

关键字:ArkUI 动画animateTotransition性能优化

解决方案

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: 动画卡顿怎么办?

检查:

  1. 是否改变了 width/height 等布局属性
  2. 是否在动画中执行复杂计算
  3. 列表项是否过多

解决:

复制代码
// ✅ 使用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

参考资料

相关推荐
遇到困难睡大觉哈哈7 小时前
Harmony os —— Data Augmentation Kit 知识问答完整示例实战拆解(从 0 跑通流式 RAG)
harmonyos·鸿蒙
鸿蒙开发工程师—阿辉7 小时前
HarmonyOS5 极致动效实验室:基本动画的使用
harmonyos·arkts·鸿蒙
kirk_wang9 小时前
Flutter Printing库在OpenHarmony上的适配实战
flutter·移动开发·跨平台·arkts·鸿蒙
kirk_wang1 天前
Flutter `video_player`库在鸿蒙端的视频播放优化:一份实用的适配指南
flutter·移动开发·跨平台·arkts·鸿蒙
waeng_luo1 天前
[鸿蒙2025领航者闯关] HarmonyOS深色模式实现
harmonyos·鸿蒙2025领航者闯关·鸿蒙6实战·开发者年度总结
waeng_luo1 天前
[鸿蒙2025领航者闯关] HarmonyOS服务卡片实战
harmonyos·鸿蒙2025领航者闯关·鸿蒙6实战·开发者年度总结
汉堡黄•᷄ࡇ•᷅1 天前
鸿蒙开发:案例集合List:多列表相互拖拽(删除/插入,偏移动效)(微暇)
华为·harmonyos·鸿蒙·鸿蒙系统
waeng_luo1 天前
[鸿蒙2025领航者闯关]使用RelationalStore实现增删改查(CRUD)操作
harmonyos·鸿蒙·#鸿蒙2025领航者闯关·#鸿蒙6实战
后端小张1 天前
【鸿蒙2025领航者闯关】从技术突破到生态共建,开发者的成长与远航
华为·wpf·生态·harmonyos·鸿蒙·鸿蒙系统·鸿蒙2025领航者试炼