HarmonyOS6 PC端动画曲线Curve全解——9种曲线视觉对比,选对曲线动画就成功了一半

做了这么多篇属性动画,我一直在强调"Curve很重要"。但到底每种Curve看起来有什么区别?说实话,光靠文字描述很难说清楚,必须亲眼看一遍才行。今天就来做一个Curve大全的可视化demo,9种曲线同时触发平移动画,直观对比它们的运动差异。
为什么Curve选择这么重要
我见过太多开发做动画只关心"动多少"和"动多久",完全不care用什么曲线。结果就是所有动画都是EaseInOut(因为这是默认值),整个App的动画看起来千篇一律,没有层次感和节奏感。
不同的Curve传递不同的"情绪"。Linear是机械的、均匀的;EaseInOut是温和的、舒适的;FastOutSlowIn是有力的、果断的;Rhythm是活泼的、弹性的。用对Curve,动画自己就会"说话"。
打个比方:同样是从左到右移动一个元素,用Linear看起来像传送带,用EaseInOut看起来像一个人走路(起步加速、到位减速),用FastOutSlowIn看起来像被推出去然后慢慢停下。三个完全不同的感觉,但代码只改了一个参数。
ArkUI的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性能强,但好的编码习惯和合理的优化策略还是值得了解的。
更多推荐


所有评论(0)