前言

在鸿蒙应用开发中,动画是提升用户体验的重要手段。而动画的"灵魂"在于曲线——它决定了动画在执行过程中的速度变化规律。一个线性匀速的动画让人感觉机械生硬,而一个带有缓入缓出的动画则显得流畅自然。

HarmonyOS 提供了 @ohos.curves 模块,它是一个专门用于创建插值曲线(Interpolation Curve)的工厂函数集合。不同于在 animateToanimation 中直接使用 Curve 枚举值,@ohos.curves 允许你以编程方式创建可复用的曲线对象(ICurve),支持三阶贝塞尔曲线、物理弹簧曲线、步进曲线、自定义曲线等。

本文以"曲线实验室"为 Demo,将 @ohos.curves 的核心能力可视化:通过 Path 组件实时绘制曲线形状、用 Slider 调节参数、播放动画预览小球沿曲线运动。全文含完整可运行代码,适合需要深入理解鸿蒙动画曲线机制的开发者。


一、curves 模块概述

1.1 什么是 curves 模块

@ohos.curves 是 HarmonyOS 的动画插值曲线模块,属于 @kit.ArkUI。它管理的是动画的时间-值映射关系——即给定一个归一化时间参数 fraction(0 到 1),返回对应的插值结果。

这个模块的核心概念是 ICurve 接口:

interface ICurve {
  interpolate(fraction: number): number;
}
  • 输入 fraction:归一化时间,范围 [0, 1]。0 表示动画开始,1 表示动画结束
  • 输出 number:插值结果。对于标准曲线,输出也在 [0, 1] 范围内;对于弹簧曲线,可能超出此范围(产生回弹/振动效果)

@ohos.curves 提供 8 个工厂函数来创建 ICurve 对象:

工厂函数 参数 说明
initCurve(curve?) Curve 枚举 从预设曲线初始化
cubicBezierCurve(x1,y1,x2,y2) 4 个 number 三阶贝塞尔曲线
springCurve(velocity,mass,stiffness,damping) 4 个 number 物理弹簧曲线
interpolatingSpring(velocity,mass,stiffness,damping) 4 个 number 插值弹簧(自包含 0→1)
springMotion(response?,dampingFraction?,overlapDuration?) 3 个可选 number 弹簧动画曲线
responsiveSpringMotion(response?,dampingFraction?,overlapDuration?) 3 个可选 number 响应式弹簧曲线
stepsCurve(count, end) number + boolean 阶梯步进曲线
customCurve(interpolate) 回调函数 完全自定义曲线

其中 springMotionresponsiveSpringMotion 是特殊的物理弹簧动画曲线,它们的插值结果不能通过 interpolate() 方法获取——这两个函数设计用于直接配合 animateTo/animation API 使用,动画时长由物理参数而非 duration 参数决定。

1.2 导入方式

import curves from '@ohos.curves';

这是 default import,来自 @kit.ArkUI。导入后可以直接使用 curves.initCurve()curves.cubicBezierCurve() 等工厂方法。

1.3 与 animateTo 中 Curve 枚举的关系

很多开发者最初接触到的是 animateTo 中的 Curve 枚举:

animateTo({ curve: Curve.EaseInOut }, () => { ... });

这里的 Curve.EaseInOut 是一个枚举值,它内部对应的是三阶贝塞尔曲线 cubicBezier(0.42, 0.0, 0.58, 1.0)。而 @ohos.curves 提供了两种更强大的能力:

  1. 将枚举值转化为 ICurve 对象curves.initCurve(Curve.EaseInOut) 返回一个可复用的曲线对象
  2. 创建枚举之外的曲线:如自定义贝塞尔参数、物理弹簧参数、步进曲线、完全自定义函数等

简单来说:Curve 枚举是"预设菜",@ohos.curves 是"开放厨房"。


二、五种核心曲线详解

2.1 预设曲线:initCurve

function initCurve(curve?: Curve): ICurve;

Curve 枚举值转化为 ICurve 对象。如果不传参数,默认为 Curve.Linear

Curve 枚举可用值(12 种):

枚举值 底层贝塞尔参数 视觉特征
Linear 匀速,对角直线
Ease (0.25, 0.1, 0.25, 1.0) 标准缓动
EaseIn (0.42, 0.0, 1.0, 1.0) 缓慢启动
EaseOut (0.0, 0.0, 0.58, 1.0) 缓慢结束
EaseInOut (0.42, 0.0, 0.58, 1.0) 缓入缓出
FastOutSlowIn (0.4, 0.0, 0.2, 1.0) 快速出慢速入
LinearOutSlowIn (0.0, 0.0, 0.2, 1.0) 线性出减速入
FastOutLinearIn (0.4, 0.0, 1.0, 1.0) 加速出线性入
ExtremeDeceleration (0.0, 0.0, 0.0, 1.0) 极度减速
Sharp (0.33, 0.0, 0.67, 1.0) 锐利过渡
Rhythm (0.7, 0.0, 0.2, 1.0) 节奏感
Smooth (0.4, 0.0, 0.4, 1.0) 平滑过渡

使用示例:

const curve: curves.ICurve = curves.initCurve(Curve.EaseInOut);
console.log(curve.interpolate(0.5).toString()); // ~0.5

interpolate(0.5)EaseInOut 曲线上返回约 0.5——因为缓入缓出曲线在中间点是对称的。而在 EaseIn 曲线上,interpolate(0.5) 会小于 0.5(动画前半段进展较慢)。

2.2 三阶贝塞尔曲线:cubicBezierCurve

function cubicBezierCurve(x1: number, y1: number, x2: number, y2: number): ICurve;

三阶贝塞尔曲线由四个控制点定义:P0(0, 0) 固定起点、P1(x1, y1)、P2(x2, y2)、P3(1, 1) 固定终点。

  • x1, x2:必须控制在 [0, 1] 范围内(超出会被裁剪)
  • y1, y2:可以超出 [0, 1] 范围,用于产生弹性/回弹效果

经典参数组合:

场景 x1 y1 x2 y2 效果
缓入(ease-in) 0.42 0.0 1.0 1.0 慢启动
缓出(ease-out) 0.0 0.0 0.58 1.0 慢结束
缓入缓出 0.42 0.0 0.58 1.0 标准缓动
弹跳效果 0.68 -0.55 0.27 1.55 y 值超出范围

当 y1 < 0 或 y2 > 1 时,曲线会超出 [0, 1] 的"标准范围",这在动画中表现为"回弹"——动画先反向运动再正向加速,或超过目标值后再回退。这就是贝塞尔曲线的弹性效果。

// 带有轻微回弹的缓出曲线
const bounceOut: curves.ICurve = curves.cubicBezierCurve(0.0, 0.0, 0.58, 1.2);
// 在 fraction=0.9 时,值可能已经超过 1.0
console.log(bounceOut.interpolate(0.9).toString()); // 可能 > 1.0

2.3 物理弹簧曲线:springCurve

function springCurve(velocity: number, mass: number, stiffness: number, damping: number): ICurve;

这是基于胡克定律和阻尼振动的物理模型,四个参数有明确的物理意义:

  • velocity(初始速度):范围 (-∞, +∞),物体在弹簧系统入口处的速度。正值为正向速度,负值为反向。通常设为 0 表示从静止开始
  • mass(质量):范围 (0, +∞),值越大惯性越大,振幅越大,恢复平衡越慢
  • stiffness(刚度):范围 (0, +∞),值越大弹簧越硬,抵抗形变能力越强,恢复平衡越快
  • damping(阻尼):范围 (0, +∞),值越大振荡衰减越快,振幅越小

参数组合示例:

场景 velocity mass stiffness damping 效果
快速响应 0 1 200 20 快到位,微振
柔和弹性 0 1 80 5 多次弹跳
强阻尼 0 1 100 30 无振荡,缓慢到位
重物感 0 3 100 10 惯性大,大幅振荡

实验技巧:降低 damping 值可以看到明显的振荡——曲线会在目标值(1.0)上下波动多次后才稳定。这是弹簧曲线与贝塞尔曲线的本质区别:贝塞尔曲线从一个点平滑过渡到另一个点,而弹簧曲线可以围绕目标值振荡

// 低阻尼弹簧:会振荡多次
const bouncySpring: curves.ICurve = curves.springCurve(0, 1, 100, 5);
// 在插值过程中,值可能多次穿过 1.0
for (let t = 0; t <= 1; t += 0.1) {
  console.log(bouncySpring.interpolate(t).toString());
}

2.4 插值弹簧曲线:interpolatingSpring

function interpolatingSpring(velocity: number, mass: number, stiffness: number, damping: number): ICurve;

参数与 springCurve 完全相同,但语义不同:

  • springCurve:输出值与输入时间相关,但不保证从 0 开始到 1 结束。它只是一个纯粹的物理模拟
  • interpolatingSpring保证生成一条从 0 到 1 的完整插值曲线,动画时长由物理参数自动决定(不受 animateToduration 参数控制)

换句话说,interpolatingSpring 是一个"自包含"的弹簧曲线——它自动调整时间映射,确保在某个物理上合理的时刻到达 1.0。这使得它非常适合用于需要弹簧效果但又有明确目标值的动画场景。

// 插值弹簧:保证 0→1 的自包含动画
const spring: curves.ICurve = curves.interpolatingSpring(0, 1, 200, 15);
// interpolate(0) ≈ 0, interpolate(1) = 1(保证)
// 但中间过程有弹簧振荡特性

2.5 步进曲线:stepsCurve

function stepsCurve(count: number, end: boolean): ICurve;

步进曲线将整个动画分成 count 个等长的区间,在每个区间内值保持不变,到达区间边界时瞬间跳变。

  • count:步数,正整数,范围 [1, +∞)。小于 1 时按 1 处理
  • end:跳变时机
    • true:在每段结束时跳变(起始位置保持不变)
    • false:在每段开始时跳变(起始位置立即改变)
// 4 步曲线,在每段结束时跳变
const steps4: curves.ICurve = curves.stepsCurve(4, true);
// interpolate(0.0) = 0
// interpolate(0.24) = 0   (还在第1段)
// interpolate(0.25) = 0.25(跳到第1段的目标值)
// interpolate(0.49) = 0.25(还在第2段)
// interpolate(0.5) = 0.5  (跳到第2段的目标值)
// interpolate(0.99) = 0.75(第4段开始)
// interpolate(1.0) = 1.0  (跳到结束)

步进曲线在视觉上产生"机械式"的分段跳变效果,适合模拟机械打字、逐帧动画、数字翻页等场景。


三、实战:动画曲线实验室页面

3.1 整体设计

实验室页面分为四个功能区:

  1. 曲线类型选择栏:5 个分类标签(预设/贝塞尔/弹簧/插值弹簧/步进),点击切换下方内容和参数面板
  2. 曲线可视化区域:使用 Shape/Path 组件实时绘制曲线形状,红色小球沿曲线运动
  3. 参数调节区:根据当前曲线类型显示不同的 Slider 控件组
  4. 动画预览区:播放/暂停/重置按钮,控制小球在曲线上从 0 到 1 的运动

3.2 核心实现

曲线可视化:Path 组件绘制

曲线可视化的关键在于将 ICurve.interpolate() 的结果转化为 SVG Path 命令。我们对 0 到 1 的归一化时间进行 80 次采样,将每个采样点连接成折线:

private canvasW: number = 336;
private canvasH: number = 200;

private buildPath(): string {
  const c: curves.ICurve = this.getCurve();
  const n: number = 80;
  let cmd: string = '';
  for (let i = 0; i <= n; i++) {
    const t: number = i / n;
    const x: number = t * this.canvasW;
    const y: number = this.canvasH - c.interpolate(t) * this.canvasH;
    if (i === 0) {
      cmd += 'M' + x.toFixed(1) + ' ' + y.toFixed(1);
    } else {
      cmd += ' L' + x.toFixed(1) + ' ' + y.toFixed(1);
    }
  }
  return cmd;
}

注意 Y 轴翻转:在 SVG 坐标系中 Y 轴向下增长,而曲线值从 0(底部)到 1(顶部)。所以我们需要用 canvasH - value * canvasH 来翻转 Y 轴。

在布局中,将 Path 放在 Shape 组件内,用 Stack 叠加:

Stack() {
  // 参考对角线(灰色虚线)
  Shape() {
    Path()
      .width(this.canvasW).height(this.canvasH)
      .commands('M0 ' + this.canvasH + ' L' + this.canvasW + ' 0')
      .stroke('#E2E8F0').strokeWidth(1)
      .strokeDashArray([4, 4])
  }
  // 曲线主体(蓝色实线)
  Shape() {
    Path()
      .width(this.canvasW).height(this.canvasH)
      .commands(this.buildPath())
      .stroke('#1677FF').strokeWidth(2.5)
  }
  // 动画小球(红色,仅在动画进行时显示)
  if (this.isPlaying || this.animProgress > 0) {
    Circle({ width: 12, height: 12 })
      .fill('#E53E3E')
      .translate({
        x: this.animProgress * this.canvasW - 6,
        y: this.canvasH - c.interpolate(this.animProgress) * this.canvasH - 6
      })
  }
}
.width(this.canvasW).height(this.canvasH)
曲线工厂:根据类别切换
private getCurve(): curves.ICurve {
  if (this.selectedCategory === 0) {
    return curves.initCurve(this.presetCurves[this.presetIndex].value);
  } else if (this.selectedCategory === 1) {
    return curves.cubicBezierCurve(this.cbX1, this.cbY1, this.cbX2, this.cbY2);
  } else if (this.selectedCategory === 2) {
    return curves.springCurve(this.spVelocity, this.spMass, this.spStiffness, this.spDamping);
  } else if (this.selectedCategory === 3) {
    return curves.interpolatingSpring(this.isVelocity, this.isMass, this.isStiffness, this.isDamping);
  } else {
    return curves.stepsCurve(this.stepsCount, this.stepsEnd);
  }
}
动画循环:setTimeout 驱动

小球沿曲线的运动通过 setTimeout 递归实现逐帧更新:

private playAnim(): void {
  if (this.isPlaying) return;
  this.isPlaying = true;
  this.animProgress = 0;
  this.tick();
}

private tick(): void {
  if (!this.isPlaying) return;
  this.animProgress += 0.006;  // 每帧前进 0.6%
  if (this.animProgress >= 1.0) {
    this.animProgress = 1.0;
    this.isPlaying = false;
    return;
  }
  this.timer = setTimeout(() => { this.tick(); }, 16); // ~60fps
}

这里没有使用 @ohos.animator 模块,因为动画预览的目的是展示曲线的速度变化特性。如果用 animateTo 配合曲线来驱动小球,就成了"用曲线解释曲线"的循环论证。使用匀速的 setTimeout 推进时间(每帧匀速前进 0.6%),让小球在曲线上自然呈现出该曲线的速度特征——这正是曲线实验室的教育目的。
在这里插入图片描述
在这里插入图片描述

参数面板:条件渲染

不同曲线类型对应不同的参数面板。例如贝塞尔曲线显示 4 个 Slider(X1/Y1/X2/Y2),步进曲线显示 1 个 Slider(步数)+ 1 个 toggle(跳变位置):

if (this.selectedCategory === 1) {
  // 贝塞尔:4 个滑块
  Row() {
    Text('X1').fontSize(12).fontColor('#64748B').width(28)
    Slider({ value: this.cbX1, min: 0, max: 1, step: 0.01 })
      .layoutWeight(1)
      .onChange((v: number) => { this.cbX1 = v; })
    Text(this.cbX1.toFixed(2)).fontSize(11).fontColor('#0F172A').width(38)
  }
  // ... Y1, X2, Y2 同理
} else if (this.selectedCategory === 4) {
  // 步进:1 个滑块 + toggle
  Row() {
    Text('步数').fontSize(12).fontColor('#64748B').width(28)
    Slider({ value: this.stepsCount, min: 1, max: 20, step: 1 })
      .layoutWeight(1)
      .onChange((v: number) => { this.stepsCount = Math.round(v); })
    Text(this.stepsCount.toFixed(0)).fontSize(11).fontColor('#0F172A').width(38)
  }
  // toggle: 起始/结束
}
预设曲线网格

12 种 Curve 枚举值用 4 列 Grid 展示,点击直接切换:

Grid() {
  ForEach(this.presetCurves, (item: PresetCurveItem, idx: number) => {
    GridItem() {
      Text(item.label)
        .fontSize(12)
        .fontColor(this.presetIndex === idx ? '#FFFFFF' : '#0F172A')
        .backgroundColor(this.presetIndex === idx ? '#1677FF' : '#F1F5F9')
        .borderRadius(6)
        .padding({ top: 8, bottom: 8 })
        .width('100%')
        .textAlign(TextAlign.Center)
        .onClick(() => { this.presetIndex = idx; })
    }
  })
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsGap(6).columnsGap(6)
.width('100%')

3.3 完整代码

以下是 CurveLabPage.ets 的完整实现(约 280 行):

import { router } from '@kit.ArkUI';
import curves from '@ohos.curves';
import { FontSize, Spacing } from '../common/Constants';

interface PresetCurveItem {
  name: string;
  label: string;
  value: curves.Curve;
}

@Entry
@Component
struct CurveLabPage {
  @State selectedCategory: number = 0;
  @State presetIndex: number = 0;
  @State cbX1: number = 0.42;
  @State cbY1: number = 0.0;
  @State cbX2: number = 0.58;
  @State cbY2: number = 1.0;
  @State spVelocity: number = 0;
  @State spMass: number = 1;
  @State spStiffness: number = 100;
  @State spDamping: number = 10;
  @State isVelocity: number = 0;
  @State isMass: number = 1;
  @State isStiffness: number = 100;
  @State isDamping: number = 10;
  @State stepsCount: number = 4;
  @State stepsEnd: boolean = true;
  @State animProgress: number = 0;
  @State isPlaying: boolean = false;

  private canvasW: number = 336;
  private canvasH: number = 200;
  private timer: number = -1;

  private categories: string[] = ['预设', '贝塞尔', '弹簧', '插值弹簧', '步进'];

  private presetCurves: PresetCurveItem[] = [
    { name: 'Linear', label: '线性', value: curves.Curve.Linear } as PresetCurveItem,
    { name: 'Ease', label: '缓动', value: curves.Curve.Ease } as PresetCurveItem,
    { name: 'EaseIn', label: '缓入', value: curves.Curve.EaseIn } as PresetCurveItem,
    { name: 'EaseOut', label: '缓出', value: curves.Curve.EaseOut } as PresetCurveItem,
    { name: 'EaseInOut', label: '缓入缓出', value: curves.Curve.EaseInOut } as PresetCurveItem,
    { name: 'FastOutSlowIn', label: '快出慢入', value: curves.Curve.FastOutSlowIn } as PresetCurveItem,
    { name: 'LinearOutSlowIn', label: '线性出慢入', value: curves.Curve.LinearOutSlowIn } as PresetCurveItem,
    { name: 'FastOutLinearIn', label: '快出线性入', value: curves.Curve.FastOutLinearIn } as PresetCurveItem,
    { name: 'ExtremeDeceleration', label: '极减速', value: curves.Curve.ExtremeDeceleration } as PresetCurveItem,
    { name: 'Sharp', label: '锐利', value: curves.Curve.Sharp } as PresetCurveItem,
    { name: 'Rhythm', label: '节奏', value: curves.Curve.Rhythm } as PresetCurveItem,
    { name: 'Smooth', label: '平滑', value: curves.Curve.Smooth } as PresetCurveItem
  ];

  // ... getCurve / buildPath / animation methods(见上文)
  // ... build() 方法(见完整源文件)
}

四、实战要点

4.1 yarn vs value:理解曲线坐标空间

贝塞尔曲线的 X 轴是时间(fraction),Y 轴是进度值。X 轴必须在 [0, 1] 内(系统强制裁剪),但 Y 轴可以超出此范围。

超出范围的 Y 值在动画中表现为:

  • y < 0:动画反向运动(先往反方向走一段再回来)
  • y > 1:动画过冲(超过目标值后再回退)

这是实现"回弹效果"的常用手法。例如,一个小球从 A 点移动到 B 点,如果使用 cubicBezier(0.68, -0.55, 0.27, 1.55),小球会先向反方向弹一下,然后快速飞向 B 点,超过 B 点后再弹回来——就像它被弹弓发射出去一样。

4.2 springCurve vs interpolatingSpring 的选择

两者的参数完全相同,区别在于"时间如何映射到值":

  • springCurve:不保证从 0 到 1 的完整映射。你可以传入一个 durationanimateTo,在这个时间窗口内观察弹簧的物理振荡。如果 duration 太短,动画结束时弹簧还没到目标值;如果太长,弹簧已经稳定了很久动画才结束
  • interpolatingSpring:自动调整时间映射,保证 0→1 的完整过渡。animateTo 中的 duration 参数被忽略,动画时长由 massstiffnessdamping 三个物理参数自动决定

选择建议

  • 如果你需要"物理真实的弹簧效果"且愿意手动计算 duration,用 springCurve
  • 如果你需要"从起始值到目标值的弹簧过渡",用 interpolatingSpring(推荐大多数场景)

4.3 stepsCurve 的 end 参数误区

end = true 不是说"在动画结束时跳变"。它的含义是:

  • end = true:每个步进区间的结束边界发生跳变。动画开始时保持初始值,在第一个步进边界跳到第一个目标值
  • end = false:每个步进区间的开始边界发生跳变。动画一开始就立即跳到第一个目标值

用一个 3 步的曲线说明:

end=true:  0%──25%──50%──75%──100%
           0     0   0.33  0.33  0.67  0.67  1.0
           ├─────┼─────┼─────┤
           
end=false: 0%──25%──50%──75%──100%
           0    0.33  0.33  0.67  0.67  1.0   1.0
              ├─────┼─────┼─────┤

注意 end=true 时,在 0% 处值是 0(保持初始值);end=false 时,在 0% 处值已经是 0.33(立即跳到第一个目标值)。

4.4 springMotion / responsiveSpringMotion 的特殊性

这两个函数返回的 ICurve 对象不应该调用 interpolate()——官方文档明确说明"the interpolation cannot be obtained using the interpolate function"。它们是设计用于直接传递给 animateToanimationcurve 参数的:

animateTo({
  duration: 1000,
  curve: curves.springMotion(0.55, 0.825, 0)
}, () => {
  this.offsetX = 200;
});

它们的动画时长由物理参数决定(不受 duration 控制),这种设计使得连续多个 springMotion 动画可以平滑地衔接——下一个动画会继承上一个动画结束时的速度。

4.5 customCurve:终极自定义

当所有内置曲线都无法满足需求时,customCurve 提供了一个完全自定义的入口:

const myCurve: curves.ICurve = curves.customCurve((fraction: number): number => {
  // 自定义映射:前半段慢,后半段快
  return fraction < 0.5
    ? fraction * fraction * 2
    : 1 - (1 - fraction) * (1 - fraction) * 2;
});

interpolate 回调函数的输入 fraction 在 [0, 1] 范围内,返回值也应在 [0, 1] 范围内(虽然可以超出)。这是实现任何非标准曲线的最直接方式。


五、典型应用场景

5.1 UI 微交互

列表项的淡入、按钮的弹性反馈、页面的过渡切换,都是曲线的典型应用场景。EaseOut 适合元素出现(快速出现然后减速停止),EaseIn 适合元素消失(缓慢开始加速消失),FastOutSlowIn 是一般过渡的首选。

5.2 游戏物理模拟

springCurveinterpolatingSpring 天然适合模拟物理世界的弹性效果——弹簧门、弹弓射击、物体落地后的弹跳。调整 damping 参数可以控制弹跳衰减速度。

5.3 数据可视化

stepsCurve 适合绘制阶梯状图表(如股价分时图、阶梯税率),Linear 适合线性关系展示。贝塞尔曲线本身也是矢量绘图中最基础的工具。

5.4 动画编辑器

如果你在开发动画编辑工具,@ohos.curves 提供的曲线对象可以序列化(保存参数)、反序列化(用参数重建曲线)、实时预览(调用 interpolate 做逐帧计算),这些特性使得它非常适合作为动画编辑器的核心引擎。


六、小结

本文以"曲线实验室"为 Demo,系统讲解了 HarmonyOS NEXT 的 @ohos.curves

  • 核心概念ICurve 接口 + interpolate(fraction) 方法,将时间映射为值
  • 5 种工厂函数initCurve(预设)、cubicBezierCurve(贝塞尔)、springCurve(物理弹簧)、interpolatingSpring(插值弹簧)、stepsCurve(步进)
  • 2 种特殊函数springMotion / responsiveSpringMotion(不支持 interpolate,直接用于 animateTo)
  • 可视化方案:使用 Path 组件绘制曲线形状 + setTimeout 驱动小球运动
  • 贝塞尔曲线弹性效果:Y 值超出 [0, 1] 实现回弹/过冲
  • 弹簧曲线的物理直觉:质量大惯性大、刚度大回复快、阻尼小振荡多

理解曲线是做好动画的基础。@ohos.curves 将曲线从 animateTo 的"附庸"提升为一等公民——你可以创建、保存、复用、可视化、甚至组合曲线对象。当你在开发复杂的动画系统时,这种"可编程曲线"的能力会极大地提升效率和表现力。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐