在 HarmonyOS 的 ArkUI 框架中,动画是提升应用交互体验的关键。首先需要澄清一个概念:ArkUI 中用于驱动状态变化触发布局更新的显式动画接口标准名称为 animateTo(而非 animationTo)。

ArkUI 提供了完整的动画系统,其中属性动画与显式动画是最核心的两种类型。它们在实现机制和适用场景上各有侧重:

一、 属性动画(animation)

属性动画是最基础易懂的动画类型。当组件的某些通用属性(如 widthheightbackgroundColoropacityscalerotatetranslate 等)发生变化时,可以通过它实现渐变过渡效果。

核心机制
开发者无需使用闭包,只需在组件的属性后链式调用 animation() 接口。只要系统检测到其绑定的可动画属性发生变化,就会自动根据配置的动画参数(如时长、曲线)添加过渡动画。

适用场景
适用于对多个可动画属性配置不同参数动画的场景,或者处理简单的状态切换(如按钮点击、悬停反馈)。

在使用 animation() 接口时,必须严格注意以下两点:

  1. 只对上方属性生效animation 接口只对写在它前面的属性生效,且对组件构造器的属性(如 Column({ space: this.space }))不生效。
  2. 支持多组独立动画:由于组件的属性链式调用是从下往上执行的,因此您可以根据调用顺序,对同一个组件的多个属性设置完全不同的动画参数。

代码示例(多属性差异化动画)

Text('ArkUI')
  .backgroundColor(0xf56c6c)
  .rotate({ angle: this.rotateValue })      // 属性1
  .translate({ x: this.translateXX })       // 属性2
  .animation({                              // 作用于上方的 translate
    curve: curves.springMotion(), 
    duration: 300 
  })
  .rotate({ angle: this.rotateValuee })     // 属性3
  .animation({                              // 作用于上方的 rotate
    curve: curves.springMotion(), 
    duration: 1200 
  })

性能优化:动画属性的性能梯队

在 PC 端或包含大量元素的复杂场景中,不同属性的动画开销差异巨大。建议遵循以下性能梯队进行开发:

  • 第一梯队(最优)opacitytranslatescalerotate。这些变化只影响 GPU 层面的合成操作,无需重新计算布局,开销极小,即使上百个元素同时动画也基本不掉帧。
  • 第二梯队(良好)backgroundColorborderColor 等颜色属性。需要框架做颜色插值计算,但在常规设备下完全不是问题。
  • 第三梯队(需注意)widthheightmarginpadding 等布局属性。这些属性的变化会触发布局重计算(layout),如果同时有几十个元素改变宽高,极易导致帧率下降。

二、 显式动画(animateTo)

显式动画是 ArkUI 提供的全局接口,专门用于指定由于闭包代码导致的状态变化插入过渡动效。

核心机制
与属性动画不同,animateTo 必须在一个回调函数(闭包)中执行。开发者需要在闭包内修改状态变量,框架会自动捕获这些变化,并为闭包前后的 UI 界面差异插值生成动画。

适用场景
适用于需要对多个可动画属性配置相同动画参数的场景、需要嵌套使用动画的场景,以及需要精细控制延迟、级联动画和完成回调的复杂动画编排。

基础属性变化与多属性联动

这是最常用的场景,通过点击按钮触发多个属性的同时变化,且闭包内的所有状态变化都会遵循相同的动画参数。

@Entry
@Component
struct BasicAnimateToExample {
  @State animate: boolean = false;
  @State rotateValue: number = 0;
  @State translateX: number = 0;
  @State opacityValue: number = 1;

  build() {
    Row() {
      Column()
        .width(100)
        .height(100)
        .backgroundColor('#317AF7')
        .borderRadius(30)
        .rotate({ angle: this.rotateValue })
        .translate({ x: this.translateX })
        .opacity(this.opacityValue)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.animate = !this.animate;
      // 核心:使用 getUIContext() 明确 UI 执行上下文
      this.getUIContext()?.animateTo({
        duration: 1000,
        curve: Curve.EaseInOut
      }, () => {
        // 闭包内改变状态,系统会自动检测差异并添加过渡动画
        this.rotateValue = this.animate ? 90 : 0;
        this.translateX = this.animate ? 100 : 0;
        this.opacityValue = this.animate ? 0.5 : 1;
      });
    })
  }
}

实例二:组件出现时的入场动画

利用 onAppear 生命周期钩子,在组件首次挂载到界面时触发入场动效(注意:不能在 aboutToAppear 中调用)。

@Entry
@Component
struct AppearAnimateToExample {
  @State rotateAngle: number = 0;

  build() {
    Column() {
      Button('入场动画示例')
        .margin(50)
        .rotate({ x: 0, y: 0, z: 1, angle: this.rotateAngle })
        .onAppear(() => {
          // 组件出现时开始做动画
          this.getUIContext()?.animateTo({
            duration: 1200,
            curve: Curve.Friction, // 阻尼曲线,效果更自然
            delay: 200,            // 延迟 200ms 执行
            iterations: -1,        // 设置为 -1 表示无限循环
            playMode: PlayMode.Alternate // 正向播放后自动反向播放
          }, () => {
            this.rotateAngle = 90;
          })
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

实例三:物理弹簧与手势跟手/离手衔接

在滑动交互中,跟手阶段使用 responsiveSpringMotion,离手后使用 springMotion 并传入当前速度,实现极具真实感的物理惯性效果。

import { curves } from '@kit.ArkUI';

@Entry
@Component
struct GestureSpringExample {
  @State cardOffset: number = 0;
  private lastCardOffset: number = 0;

  build() {
    Column() {
      Button('拖拽我')
        .translate({ x: this.cardOffset })
        .gesture(
          PanGesture()
            .onActionUpdate((event: GestureEvent | undefined) => {
              if (event) {
                // 1. 跟手阶段:直接修改状态,配合响应弹簧曲线
                this.getUIContext()?.animateTo({
                  curve: curves.responsiveSpringMotion()
                }, () => {
                  this.cardOffset = this.lastCardOffset + event.offsetX;
                });
              }
            })
            .onActionEnd((event: GestureEvent | undefined) => {
              if (event) {
                // 2. 离手阶段:使用普通弹簧曲线,并传入当前速度实现惯性
                this.getUIContext()?.animateTo({
                  curve: curves.springMotion(0.5, 0.8, event.velocity) 
                }, () => {
                  // 这里可以写回弹到原点或吸附到目标位置的逻辑
                  this.cardOffset = 0; 
                  this.lastCardOffset = this.cardOffset;
                });
              }
            })
        )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

实例四:立即中断正在进行的动画

如果在动画播放过程中需要立刻停止并定格在某个状态,可以使用 duration: 0 配合属性重置。

@Entry
@Component
struct StopAnimateExample {
  @State rotateAngle: number = 0;

  build() {
    Column() {
      Button('停止旋转')
        .margin(50)
        .rotate({ x: 0, y: 0, z: 1, angle: this.rotateAngle })
        .onClick(() => {
          // 在 duration 为 0 的动画中修改属性
          // 可以立即停止该属性之前的动画,并按新设置的属性显示
          this.getUIContext()?.animateTo({ duration: 0 }, () => {
            this.rotateAngle = 0;
          })
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

三、 两者的核心区别

  1. 触发方式:属性动画是“被动触发”,只需在属性后绑定 animation(),状态改变即触发;显式动画是“主动触发”,必须显式调用 animateTo 并在闭包中修改状态。
  2. 作用域animation 接口只会作用于在其之上的属性调用;而 animateTo 闭包内改变的任何可动画属性,都会遵循相同的动画参数。

四、 进阶特性:动画衔接

显式动画具备强大的动画衔接能力。当存在正在运行的动画时,如果用户行为(如连续快速点击)导致属性终点值发生变化,开发者仅需在 animateTo 动画闭包中再次改变属性值,系统会自动衔接之前的动画和当前的动画,平滑过渡到新的终点值,避免产生停顿感或骤变。

1、 高级动画曲线:物理弹簧曲线

除了常规的线性或缓入缓出曲线,ArkUI 提供了极具真实感的物理弹簧曲线,常用于列表滑动、弹窗弹出等需要惯性和阻尼效果的场景。

  1. springMotion(离手惯性曲线)
    适用于手指离开屏幕后的惯性滑动。它需要接收一个 velocity(速度)参数,能够根据上一帧的手势速度,生成带有自然减速效果的动画。
  2. responsiveSpringMotion(跟手响应曲线)
    适用于手指拖拽过程中的实时反馈。它响应极快,没有明显的延迟,能让 UI 像粘在手指上一样跟手。

代码示例

// 跟手阶段:使用 responsiveSpringMotion
this.getUIContext().animateTo({
  curve: curves.responsiveSpringMotion(0.5, 0.8)
}, () => {
  this.translateX = event.offsetX;
});

// 离手阶段:使用 springMotion 并传入当前速度
this.getUIContext().animateTo({
  curve: curves.springMotion(0.5, 0.8, currentVelocity)
}, () => {
  this.translateX = targetX; // 吸附到目标位置
});
2、 转场动画(TransitionEffect)

当组件被创建(出现)或销毁(消失)时,可以使用 transition 属性为其添加出场/入场动画。

核心机制
通过 TransitionEffect 组合不同的效果(如 opacitytranslatescale),并绑定到组件的 transition 属性上。当组件通过 if/else 或 ForEach 增删时,动画会自动触发。

代码示例

if (this.isShow) {
  Column() {
    Text('弹窗内容')
  }
  .transition(
    TransitionEffect.OPACITY // 透明度渐变
      .combine(TransitionEffect.translate({ y: 100 })) // 组合Y轴平移
      .animation({ duration: 300, curve: Curve.EaseOut })
  )
}
3、 手势与动画的深度联动

在拖拽、缩放等复杂交互中,动画与手势是密不可分的。

最佳实践

  1. 拖拽中(onActionUpdate):直接修改状态变量,配合 responsiveSpringMotion 实现无延迟跟手。
  2. 拖拽结束(onActionEnd):获取 event.velocity,在 animateTo 中使用 springMotion 实现惯性滑动或回弹。
  3. 防抖与节流:在高频手势事件中,避免在 onActionUpdate 中频繁调用 animateTo,直接赋值即可;仅在 onActionEnd 中调用 animateTo 进行收尾动画。
4、 全局动画配置与性能监控
  1. 关闭全局动画
    在自动化测试或低端设备降级场景中,可以通过 AppStorage 或 Configuration 动态关闭全局动画,提升性能。
    // 在 EntryAbility 中配置
    Configuration.setGlobalAnimationEnabled(false);
  2. 动画性能监控
    使用 onFinish 回调配合 hilog 记录动画耗时。如果动画频繁掉帧,应检查是否触发了不必要的布局重排(如动画过程中改变了 width/height),建议将重排动画替换为 translate/scale/opacity 等纯渲染层动画。

五、 开发注意事项

  1. UI 上下文依赖animateTo 依赖 UI 的执行上下文,不可在 UI 上下文不明确的地方(如 aboutToAppear 或 aboutToDisappear 中)使用,建议通过 getUIContext()?.animateTo 来明确上下文。
  2. 布局类属性动画:对于改变布局类属性(如宽高)的动画,内容(如文字、Canvas)通常会直接跳转到最终状态。如果希望内容跟随宽高变化,可以使用 renderFit 属性进行配置。
  3. 性能优化:在动画性能的优先级排序中,opacity(透明度)优于 transform(平移/缩放/旋转),优于 backgroundColor,最后才是 width/height。因为 opacity 是纯 GPU 合成层操作,不触发布局重排和重绘,是大规模列表动画的首选方案。
Logo

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

更多推荐