动画曲线Curve详解封面图

做了这么多篇属性动画,我一直在强调"Curve很重要"。但到底每种Curve看起来有什么区别?说实话,光靠文字描述很难说清楚,必须亲眼看一遍才行。今天就来做一个Curve大全的可视化demo,9种曲线同时触发平移动画,直观对比它们的运动差异。

为什么Curve选择这么重要

我见过太多开发做动画只关心"动多少"和"动多久",完全不care用什么曲线。结果就是所有动画都是EaseInOut(因为这是默认值),整个App的动画看起来千篇一律,没有层次感和节奏感。

不同的Curve传递不同的"情绪"。Linear是机械的、均匀的;EaseInOut是温和的、舒适的;FastOutSlowIn是有力的、果断的;Rhythm是活泼的、弹性的。用对Curve,动画自己就会"说话"。

打个比方:同样是从左到右移动一个元素,用Linear看起来像传送带,用EaseInOut看起来像一个人走路(起步加速、到位减速),用FastOutSlowIn看起来像被推出去然后慢慢停下。三个完全不同的感觉,但代码只改了一个参数。

ArkUI的Curve枚举全览

动画曲线Curve详解概念图

ArkUI提供了以下Curve枚举值,这次我们选取9种来做对比:

  • Curve.Linear:匀速运动,从开始到结束速度不变。
  • Curve.Ease:慢入慢出,和EaseInOut类似但过渡更柔和。
  • Curve.EaseIn:慢入快出,开始很慢,后面越来越快。
  • Curve.EaseOut:快入慢出,开始很快,最后慢慢停下。
  • Curve.EaseInOut:慢入慢出,最经典的缓动曲线,两头慢中间快。
  • Curve.FastOutSlowIn:快出慢入,快速启动然后慢慢到位,Material Design的经典曲线。
  • Curve.FastOutLinearIn:快出匀入,快速启动然后匀速。
  • Curve.LinearOutSlowIn:匀出慢入,匀速启动然后慢慢停下。
  • Curve.Rhythm:弹性曲线,会过冲然后弹回来。

可视化对比的设计思路

要做直观的曲线对比,最好的方式是让9个小球同时做同样的运动(比如从左到右平移),每个小球使用不同的Curve。这样你一眼就能看出哪个小球先到、哪个后到、哪个在"弹"。

布局上用9行,每行一个小球+曲线名称。点击"开始"按钮后,所有小球同时向右平移,然后观察它们到达的先后顺序和运动方式。

完整案例代码

@Entry
@Component
struct CurveCompareDemo {
  @State offsets: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0]
  @State hasAnimated: boolean = false
  @State animDuration: number = 1500
  @State moveDistance: number = 200

  private curveNames: string[] = [
    'Linear', 'Ease', 'EaseIn', 'EaseOut', 'EaseInOut',
    'FastOutSlowIn', 'FastOutLinearIn', 'LinearOutSlowIn', 'Rhythm'
  ]
  private curveColors: string[] = [
    '#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#3498db',
    '#9b59b6', '#1abc9c', '#fd79a8', '#6c5ce7'
  ]
  private curveDescriptions: string[] = [
    '匀速,机械感',
    '柔和缓动',
    '慢入快出,加速感',
    '快入慢出,减速感',
    '慢入慢出,最常用',
    '快出慢入,有力感',
    '快出匀入,冲刺感',
    '匀出慢入,刹车感',
    '弹性过冲,活泼感'
  ]

  getCurve(index: number): Curve {
    let curves: Curve[] = [
      Curve.Linear, Curve.Ease, Curve.EaseIn, Curve.EaseOut,
      Curve.EaseInOut, Curve.FastOutSlowIn, Curve.FastOutLinearIn,
      Curve.LinearOutSlowIn, Curve.Rhythm
    ]
    return curves[index]
  }

  build() {
    Column({ space: 12 }) {
      Text('HarmonyOS6 PC 动画曲线 Curve 全解')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1a1a2e')
        .margin({ top: 16 })

      Text('9种Curve同时对比,观察运动差异')
        .fontSize(14)
        .fontColor('#888888')

      // 控制面板
      Row({ space: 12 }) {
        Button(this.hasAnimated ? '再来一次' : '开始对比')
          .fontSize(14)
          .height(38)
          .backgroundColor('#6c5ce7')
          .onClick(() => {
            this.startComparison()
          })

        Button('重置')
          .fontSize(14)
          .height(38)
          .backgroundColor('#636e72')
          .onClick(() => {
            this.resetAll()
          })
      }

      // 时长调节
      Row({ space: 8 }) {
        Text('动画时长:')
          .fontSize(13)
          .fontColor('#666666')
        Button('1s')
          .fontSize(12)
          .height(28)
          .backgroundColor(this.animDuration === 1000 ? '#e74c3c' : '#dfe6e9')
          .fontColor(this.animDuration === 1000 ? '#ffffff' : '#2d3436')
          .onClick(() => { this.animDuration = 1000 })
        Button('1.5s')
          .fontSize(12)
          .height(28)
          .backgroundColor(this.animDuration === 1500 ? '#e74c3c' : '#dfe6e9')
          .fontColor(this.animDuration === 1500 ? '#ffffff' : '#2d3436')
          .onClick(() => { this.animDuration = 1500 })
        Button('2.5s')
          .fontSize(12)
          .height(28)
          .backgroundColor(this.animDuration === 2500 ? '#e74c3c' : '#dfe6e9')
          .fontColor(this.animDuration === 2500 ? '#ffffff' : '#2d3436')
          .onClick(() => { this.animDuration = 2500 })
      }

      // 曲线对比区域
      Scroll() {
        Column({ space: 6 }) {
          ForEach(this.curveNames, (name: string, index: number) => {
            Row({ space: 8 }) {
              // 曲线名称
              Column() {
                Text(name)
                  .fontSize(11)
                  .fontWeight(FontWeight.Medium)
                  .fontColor(this.curveColors[index])
                Text(this.curveDescriptions[index])
                  .fontSize(9)
                  .fontColor('#b2bec3')
              }
              .width(100)
              .alignItems(HorizontalAlign.End)

              // 运动轨道
              Stack({ alignContent: Alignment.Start }) {
                // 轨道背景
                Row()
                  .width(this.moveDistance + 40)
                  .height(4)
                  .borderRadius(2)
                  .backgroundColor('#ecf0f1')

                // 小球
                Column()
                  .width(24)
                  .height(24)
                  .borderRadius(12)
                  .backgroundColor(this.curveColors[index])
                  .shadow({ radius: 4, color: '#00000020', offsetX: 0, offsetY: 2 })
                  .translate({ x: this.offsets[index], y: -10 })
              }
              .width(this.moveDistance + 40)
              .height(24)
            }
            .padding({ top: 4, bottom: 4 })
          })
        }
        .padding(12)
        .borderRadius(12)
        .backgroundColor('#f8f9fa')
      }
      .layoutWeight(1)

      // 观察指南
      Column({ space: 4 }) {
        Text('观察要点')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#2d3436')
          .alignSelf(ItemAlign.Start)
        Text('• Linear 匀速无变化,最先到达终点附近')
          .fontSize(11).fontColor('#666666').alignSelf(ItemAlign.Start)
        Text('• EaseIn 开始最慢,后半段加速冲刺')
          .fontSize(11).fontColor('#666666').alignSelf(ItemAlign.Start)
        Text('• EaseOut 起步最快,结尾慢慢刹停')
          .fontSize(11).fontColor('#666666').alignSelf(ItemAlign.Start)
        Text('• Rhythm 唯一会冲过终点再弹回来的')
          .fontSize(11).fontColor('#666666').alignSelf(ItemAlign.Start)
        Text('• FastOutSlowIn 和 EaseOut 相似但起步更猛')
          .fontSize(11).fontColor('#666666').alignSelf(ItemAlign.Start)
      }
      .padding(12)
      .borderRadius(12)
      .backgroundColor('#f0f0f0')
      .width('90%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#ffffff')
  }

  startComparison() {
    this.hasAnimated = true
    // 先重置位置
    this.offsets = [0, 0, 0, 0, 0, 0, 0, 0, 0]

    // 延迟一帧确保重置生效
    setTimeout(() => {
      for (let i = 0; i < 9; i++) {
        let idx = i
        animateTo({
          duration: this.animDuration,
          curve: this.getCurve(idx),
          iterations: 1
        }, () => {
          let newArr = [...this.offsets]
          newArr[idx] = this.moveDistance
          this.offsets = newArr
        })
      }
    }, 50)
  }

  resetAll() {
    this.hasAnimated = false
    animateTo({
      duration: 400,
      curve: Curve.EaseOut,
      iterations: 1
    }, () => {
      this.offsets = [0, 0, 0, 0, 0, 0, 0, 0, 0]
    })
  }
}

运行效果如图

在这里插入图片描述

代码解析

这个demo的核心是startComparison函数——一个for循环为9个小球分别触发animateTo,每个使用不同的Curve。因为9个animateTo几乎在同一帧触发,所以9个小球同时开始运动,但由于Curve不同,运动的"节奏"完全不一样。

这里有个重要的技术点:每次animateTo只修改this.offsets[idx]这一个元素。因为animateTo追踪的是具体被修改的属性,所以9个animateTo之间互不干扰。每个小球独立地按照自己的Curve运动。

观察指南部分是我觉得最有价值的地方。很多人看完动画对比之后还是不知道该选哪个曲线,所以我把每种曲线的"性格"总结成了一句话。在实际开发中,你可以直接根据这些描述来选曲线。

每种曲线的最佳使用场景

Linear适合Loading旋转、进度条匀速增长这类需要"稳定节奏"的场景。不适合做位移和缩放动画,因为匀速运动看起来太机械了。

EaseInOut是"万金油"曲线,90%的动画用它都不会出错。如果你不确定用什么曲线,就用EaseInOut。它在HarmonyOS中被设为默认曲线不是没有道理的。

EaseIn适合"离开"类动画——元素加速飞走,给人一种"不回头"的感觉。比如删除一个item时,item加速滑出屏幕。

EaseOut适合"到达"类动画——元素快速出现然后慢慢到位。比如新消息气泡弹出、搜索结果滑入。

FastOutSlowIn比EaseOut更有"力量感",适合需要强调"快速响应"的交互。按钮点击后的展开效果、面板弹出用它特别好。

Rhythm是"气氛组"成员,适合需要吸引注意力的场景。但不适合频繁使用——如果页面上所有动画都是弹性的,用户会觉得整个界面在"抖"。

曲线选择的"反模式"

有几个常见的曲线选择错误值得提醒。

第一,所有动画都用Linear。Linear的问题在于它违反物理规律——真实世界中几乎没有东西是匀速运动的。所以Linear做的位移和缩放动画看起来总是"假假的"。

第二,所有动画都用同一个曲线。不同的交互动作需要不同的"情绪",一个App里的"出现"动画和"消失"动画应该用不同的曲线(出现用EaseOut,消失用EaseIn)。

第三,弹性曲线滥用。Rhythm很酷,但用在错误的场景(比如表单验证错误提示)会让用户觉得不够严肃。弹性曲线留给有趣、轻松的场景。

PC端大屏下的曲线感知

PC端的大屏对曲线差异的感知更明显。在手机上,9个小球的运动距离可能就200像素,差异不太容易看出来。但在PC端,同样的动画可以拉宽到400-500像素,每种曲线的差异一目了然。

这也是为什么PC端动画更需要仔细选Curve——用户更容易发现"不对劲"的动画曲线。一个本该用EaseOut的弹窗出现效果,如果用了Linear,PC端用户会明显感觉到"弹窗是匀速冒出来的,好奇怪"。

写在最后

曲线选择是动画设计的"最后一块拼图"。同样的属性变化、同样的时长,换一个Curve就是完全不同的感受。今天这个9曲线对比demo建议你实际跑一遍,亲眼看看每个曲线的运动差异,比看十篇文章都有用。

下一篇是这个属性动画系列的收尾篇,来聊聊PC端的动画性能优化。虽然PC性能强,但好的编码习惯和合理的优化策略还是值得了解的。

Logo

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

更多推荐