鸿蒙新特性实战:@ohos.curves 打造动画曲线实验室
前言
在鸿蒙应用开发中,动画是提升用户体验的重要手段。而动画的"灵魂"在于曲线——它决定了动画在执行过程中的速度变化规律。一个线性匀速的动画让人感觉机械生硬,而一个带有缓入缓出的动画则显得流畅自然。
HarmonyOS 提供了 @ohos.curves 模块,它是一个专门用于创建插值曲线(Interpolation Curve)的工厂函数集合。不同于在 animateTo 或 animation 中直接使用 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) |
回调函数 | 完全自定义曲线 |
其中 springMotion 和 responsiveSpringMotion 是特殊的物理弹簧动画曲线,它们的插值结果不能通过 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 提供了两种更强大的能力:
- 将枚举值转化为 ICurve 对象:
curves.initCurve(Curve.EaseInOut)返回一个可复用的曲线对象 - 创建枚举之外的曲线:如自定义贝塞尔参数、物理弹簧参数、步进曲线、完全自定义函数等
简单来说: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 的完整插值曲线,动画时长由物理参数自动决定(不受animateTo的duration参数控制)
换句话说,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 整体设计
实验室页面分为四个功能区:
- 曲线类型选择栏:5 个分类标签(预设/贝塞尔/弹簧/插值弹簧/步进),点击切换下方内容和参数面板
- 曲线可视化区域:使用 Shape/Path 组件实时绘制曲线形状,红色小球沿曲线运动
- 参数调节区:根据当前曲线类型显示不同的 Slider 控件组
- 动画预览区:播放/暂停/重置按钮,控制小球在曲线上从 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 的完整映射。你可以传入一个
duration给animateTo,在这个时间窗口内观察弹簧的物理振荡。如果 duration 太短,动画结束时弹簧还没到目标值;如果太长,弹簧已经稳定了很久动画才结束 - interpolatingSpring:自动调整时间映射,保证 0→1 的完整过渡。
animateTo中的duration参数被忽略,动画时长由mass、stiffness、damping三个物理参数自动决定
选择建议:
- 如果你需要"物理真实的弹簧效果"且愿意手动计算 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"。它们是设计用于直接传递给 animateTo 或 animation 的 curve 参数的:
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 游戏物理模拟
springCurve 和 interpolatingSpring 天然适合模拟物理世界的弹性效果——弹簧门、弹弓射击、物体落地后的弹跳。调整 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 的"附庸"提升为一等公民——你可以创建、保存、复用、可视化、甚至组合曲线对象。当你在开发复杂的动画系统时,这种"可编程曲线"的能力会极大地提升效率和表现力。
更多推荐



所有评论(0)