问题描述

如何实现流畅的页面转场动画?如何给组件添加显隐动画?如何实现数字变化的动画效果?

关键字: animateTo、transition、页面转场、组件动画、数字动画

解决方案

完整代码

/**
 * 组件显隐动画
 */
@Component
struct AnimationDemo {
  @State visible: boolean = false;
  
  build() {
    Column({ space: 20 }) {
      Button('切换显示')
        .onClick(() => {
          animateTo({
            duration: 300,
            curve: Curve.EaseInOut
          }, () => {
            this.visible = !this.visible;
          })
        })
      
      if (this.visible) {
        Column() {
          Text('动画内容')
            .fontSize(18)
        }
        .width(200)
        .height(100)
        .backgroundColor('#ff6b6b')
        .borderRadius(12)
        .transition({
          type: TransitionType.Insert,
          opacity: 0,
          translate: { y: -50 }
        })
        .transition({
          type: TransitionType.Delete,
          opacity: 0,
          translate: { y: 50 }
        })
      }
    }
    .padding(20)
  }
}
​
/**
 * 数字变化动画
 */
@Component
struct NumberAnimationDemo {
  @State number: number = 0;
  @State displayNumber: number = 0;
  private timer: number = -1;
  
  animateNumber(target: number) {
    const start = this.displayNumber;
    const diff = target - start;
    const duration = 1000;
    const steps = 60;
    const stepValue = diff / steps;
    let currentStep = 0;
    
    if (this.timer >= 0) {
      clearInterval(this.timer);
    }
    
    this.timer = setInterval(() => {
      currentStep++;
      if (currentStep >= steps) {
        this.displayNumber = target;
        clearInterval(this.timer);
        this.timer = -1;
      } else {
        this.displayNumber = start + stepValue * currentStep;
      }
    }, duration / steps);
  }
  
  aboutToDisappear() {
    if (this.timer >= 0) {
      clearInterval(this.timer);
    }
  }
  
  build() {
    Column({ space: 20 }) {
      Text(`¥${this.displayNumber.toFixed(2)}`)
        .fontSize(48)
        .fontWeight(FontWeight.Bold)
        .fontColor('#ff6b6b')
      
      Row({ space: 12 }) {
        Button('增加1000')
          .onClick(() => {
            this.number += 1000;
            this.animateNumber(this.number);
          })
        
        Button('减少500')
          .onClick(() => {
            this.number -= 500;
            this.animateNumber(this.number);
          })
      }
    }
    .padding(20)
  }
}
​
/**
 * 列表项动画
 */
@Component
struct ListItemAnimation {
  @State items: string[] = ['项目1', '项目2', '项目3'];
  
  build() {
    Column({ space: 12 }) {
      Button('添加项目')
        .onClick(() => {
          animateTo({ duration: 300 }, () => {
            this.items.push(`项目${this.items.length + 1}`);
          })
        })
      
      List({ space: 8 }) {
        ForEach(this.items, (item: string, index: number) => {
          ListItem() {
            Row() {
              Text(item)
                .fontSize(16)
                .layoutWeight(1)
              
              Button('删除')
                .fontSize(14)
                .backgroundColor('#ff6b6b')
                .onClick(() => {
                  animateTo({ duration: 300 }, () => {
                    this.items.splice(index, 1);
                  })
                })
            }
            .width('100%')
            .padding(16)
            .backgroundColor(Color.White)
            .borderRadius(8)
          }
          .transition({
            type: TransitionType.All,
            opacity: 0,
            translate: { x: -100 }
          })
        })
      }
      .layoutWeight(1)
    }
    .padding(16)
    .width('100%')
    .height('100%')
  }
}
​
/**
 * 旋转加载动画
 */
@Component
struct RotateAnimation {
  @State angle: number = 0;
  private timer: number = -1;
  
  startRotate() {
    this.timer = setInterval(() => {
      animateTo({ duration: 1000, curve: Curve.Linear }, () => {
        this.angle += 360;
      })
    }, 1000);
  }
  
  stopRotate() {
    if (this.timer >= 0) {
      clearInterval(this.timer);
      this.timer = -1;
    }
  }
  
  aboutToAppear() {
    this.startRotate();
  }
  
  aboutToDisappear() {
    this.stopRotate();
  }
  
  build() {
    Column() {
      Image($r('app.media.icon'))
        .width(50)
        .height(50)
        .rotate({ angle: this.angle })
    }
  }
}

使用示例

// 使用组件动画
animateTo({ duration: 300, curve: Curve.EaseInOut }, () => {
  this.visible = !this.visible;
});
​
// 使用数字动画
this.animateNumber(5000);
​
// 列表添加动画
animateTo({ duration: 300 }, () => {
  this.items.push('新项目');
});

原理解析

1. animateTo 闭包动画

animateTo({ duration: 300, curve: Curve.EaseInOut }, () => {
  this.visible = !this.visible; // 状态变化会触发动画
})
  • 闭包内的状态变化会产生动画
  • duration 指定动画时长(毫秒)
  • curve 指定动画曲线

2. transition 转场

.transition({
  type: TransitionType.Insert,
  opacity: 0,
  translate: { y: -50 }
})
  • Insert:组件插入时的动画
  • Delete:组件删除时的动画
  • All:插入和删除都使用相同动画

3. 数字动画原理

  • 使用 setInterval 逐帧更新数字
  • 计算每帧的增量(目标值-当前值)/帧数
  • 达到目标值后清除定时器
  • 组件销毁时必须清除定时器

4. 常用动画曲线

  • Curve.Linear:线性,匀速
  • Curve.EaseInOut:先加速后减速,最自然
  • Curve.Friction:摩擦力,有弹性
  • Curve.Sharp:快速开始和结束

最佳实践

  1. 动画时长: 通常使用 300ms,过长会显得拖沓
  2. 动画曲线: EaseInOut 最自然,Friction 有弹性效果
  3. 性能: 避免同时执行大量动画
  4. 状态管理: 动画相关状态用 @State
  5. 清理资源: 组件销毁时清除定时器

避坑指南

  1. 忘记 animateTo: 直接修改状态不会有动画
  2. transition 位置: transition 要放在组件上,不是容器
  3. 定时器泄漏: 忘记 clearInterval 导致内存泄漏
  4. 动画冲突: 同一属性不要同时执行多个动画
  5. 性能问题: 列表项过多时避免使用 transition

效果展示

  • 组件动画:淡入淡出 + 位移,从上方滑入,向下方滑出
  • 数字动画:平滑递增/递减,1 秒内完成变化
  • 列表动画:添加/删除带动画效果,从左侧滑入
  • 旋转动画:持续旋转,用于加载提示
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐