HarmonyOS鸿蒙Next中怎样实现流畅的动画效果?

HarmonyOS鸿蒙Next中怎样实现流畅的动画效果? 遇到的问题:

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

简单易懂

更多关于HarmonyOS鸿蒙Next中怎样实现流畅的动画效果?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


解决方案

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

参考资料

在HarmonyOS Next中,实现流畅动画主要依靠ArkUI的动画API。使用animateTo方法,通过设置duration、curve等参数控制动画时长与运动曲线。关键点在于利用状态变量驱动UI属性变化,系统会自动完成过渡渲染。推荐使用显式动画,并合理设置动画曲线(如ease-in-out)来优化视觉流畅度。避免在动画过程中进行高耗时计算。

在HarmonyOS Next中实现流畅动画,核心在于合理使用ArkUI的动画API并遵循性能最佳实践。

1. animateTo 与 transition 的选择

  • animateTo:用于显式动画,适合需要精确控制时序、组合多个属性变化的场景。例如复杂组件的入场/出场动画。
  • transition:用于隐式动画,当状态变量(@State, @Prop等)改变时自动触发。适合简单属性过渡(如显示/隐藏、尺寸变化)。

2. 优化动画性能的关键点

  • 使用硬件加速:确保动画属性(如opacity, transform)由GPU渲染,避免修改width/height等触发布局重绘的属性。
  • 帧率稳定:在animateTo中指定curve: Curve.EaseOut等内置曲线,避免自定义复杂曲线计算。对于列表滚动等连续动画,使用animation修饰符并设置duration: 0以实现60fps跟随。
  • 列表动画优化:对List/Grid使用animateTo更新数据源,而非逐项修改。配合ListItemTransition实现条目增删动画,并利用reuseId复用组件减少创建开销。

3. 深色模式切换过渡 通过状态变量控制主题色,并用transition包装颜色属性:

[@State](/user/State) isDark: boolean = false

build() {
  Column()
    .backgroundColor(this.isDark ? Color.Black : Color.White)
    .transition({ type: TransitionType.All, curve: Curve.EaseInOut })
}

4. 其他建议

  • 复杂动画考虑Canvas绘制,避免组件树频繁更新。
  • 使用@Reusable装饰器减少动画组件重建。
  • 监控PerformanceObserver定位丢帧原因。

通过正确选择动画API、优化属性变更方式及利用系统级优化,可显著提升动画流畅度。

回到顶部