我曾经花了一整个下午,就为了调一个按钮的点击动画。代码逻辑没问题,状态切换没问题,时长也没问题——但就是觉得"不对味"。后来我把动画曲线从 Linear 换成了 FastOutSlowIn,瞬间就舒服了。那一刻我才真正理解:动画曲线才是动效的灵魂。
很多人写动画的时候,习惯性地用 Linear 或者 EaseInOut,然后觉得所有动画都"差不多"。其实 HarmonyOS6 PC 提供了相当丰富的曲线类型,每种曲线都有自己独特的"性格"。选对了曲线,300毫秒的动画比1秒的还好看;选错了曲线,再长的动画也像是在凑时间。
今天这篇文章,我把 Curve 枚举里的每种曲线都讲透,配上对比代码,让你能直观感受不同曲线的差异。
什么是动画曲线
在我们深入之前,先建立一个直觉。动画曲线本质上就是一个函数:输入是时间进度(0到1),输出是动画进度(也是0到1)。
如果是 Linear(线性)曲线,时间过了一半,动画也走了一半——匀速运动。如果是 EaseOut 曲线,时间过了一半,动画可能已经走了80%——前面快后面慢。曲线决定了动画在每一刻的"速度",这就是为什么同样的起止状态和时长,不同曲线给人的感觉完全不同。
你可以把动画曲线想象成开车的油门控制。Linear 是匀速踩油门,车匀加速匀减速。EaseIn 是先轻踩后重踩,车慢慢起步然后猛加速。EaseOut 是先重踩后松油门,车猛冲出去然后慢慢停下。
Curve 枚举全览
HarmonyOS6 PC 的 Curve 枚举提供了以下常用曲线类型:
Curve.Linear— 线性,匀速Curve.Ease— 默认缓动,略带 EaseIn 和 EaseOutCurve.EaseIn— 缓入,慢起步Curve.EaseOut— 缓出,慢停车Curve.EaseInOut— 缓入缓出,两头慢中间快Curve.FastOutSlowIn— 快出慢入,前段快速后段缓慢Curve.LinearOutSlowIn— 线性起步,慢速结束Curve.FastOutLinearIn— 快速起步,线性结束Curve.ExtremeDeceleration— 极端减速Curve.Sharp— 尖锐曲线Curve.Rhythm— 节奏曲线
逐个击破:每种曲线的性格
Linear:毫无感情的匀速机器
Column().width(60).height(60).backgroundColor('#FF6B6B').borderRadius(12).translate({x:this.offsetX}).animation({duration:1000,curve:Curve.Linear})Linear 曲线的输出和时间完全成正比。时间过了30%,动画就走了30%;时间过了70%,动画就走了70%。没有任何加速或减速。
这种"诚实"的匀速运动,在自然界中几乎不存在——现实中的物体要么在加速,要么在减速,很少有真正匀速的。所以 Linear 动画给人的感觉是"机械的"、“人工的”。
但这不代表 Linear 没用。它在以下场景非常合适:
- 持续旋转(风扇、loading转圈),匀速旋转才自然
- 进度条匀速增长(表示稳定的处理过程)
- 无限循环动画(避免每个循环之间有速度突变)
// 匀速旋转的loadingColumn().width(40).height(40).rotate({angle:this.rotateAngle}).animation({duration:1000,curve:Curve.Linear,iterations:-1})EaseIn:优雅的起步
Column().width(60).height(60).backgroundColor('#FFD93D').borderRadius(12).translate({x:this.offsetX}).animation({duration:1000,curve:Curve.EaseIn})EaseIn 曲线在开始时变化很慢,然后逐渐加速,到结束时达到最快速度。数学上近似于二次函数t²或三次函数t³。
这种曲线适合"离场"动画——元素要离开屏幕时,先慢慢起步,然后加速飞走。就像你扔一个球出去,球在出手的那一刻是逐渐加速的。
// 元素向右飞出屏幕Button('发送').translate({x:this.flyOffX}).animation({duration:500,curve:Curve.EaseIn}).onClick(()=>{this.flyOffX=500// 加速飞出})不适合的场景是"进场"或"点击反馈"——一个元素从外面慢慢起步飞进来,会给人一种"犹豫"的感觉。
EaseOut:稳重的停车
Column().width(60).height(60).backgroundColor('#4ECDC4').borderRadius(12).translate({x:this.offsetX}).animation({duration:1000,curve:Curve.EaseOut})EaseOut 和 EaseIn 恰好相反:开始变化很快,然后逐渐减速,到结束时完全停下来。数学上近似于1 - (1-t)²或1 - (1-t)³。
这是最常用的曲线类型,没有之一。因为绝大多数"进场"动画都适合 EaseOut——元素从外面快速滑入,然后缓缓停在目标位置。这和我们在现实世界中看到的情景一致:一个物体滑过来,摩擦力让它慢慢停下。
// 通知从右侧滑入Column(){Text('新消息')}.translate({x:this.notifyX}).animation({duration:400,curve:Curve.EaseOut}).onAppear(()=>{this.notifyX=300// 初始在屏幕外this.notifyX=0// 滑入到目标位置})EaseOut 也特别适合点击反馈。按钮被按下后弹回的过程,用 EaseOut 就很自然——快速弹起,慢慢稳定。
EaseInOut:对称的呼吸感
Column().width(60).height(60).backgroundColor('#6BCB77').borderRadius(12).translate({x:this.offsetX}).animation({duration:1000,curve:Curve.EaseInOut})EaseInOut 结合了 EaseIn 和 EaseOut 的特点:起步慢,中间快,结束慢。数学上就是 EaseIn 和 EaseOut 的分段组合。
这种曲线最大的特点是"对称感"。它没有明显的"快起步"或"快结束",整体感觉比较平衡。适合以下场景:
- 状态切换(展开/折叠、开/关)
- 大小变化(放大/缩小)
- 透明度渐变(淡入/淡出)
// 手风琴展开效果Column(){// 折叠内容}.height(this.expandedHeight).animation({duration:350,curve:Curve.EaseInOut})EaseInOut 的对称性既是优点也是缺点。它很"安全",用在哪儿都不会出错,但也缺少个性。如果你想要更鲜明的动效风格,可以考虑 FastOutSlowIn。
FastOutSlowIn:前冲后缓的活力曲线
Column().width(60).height(60).backgroundColor('#4D96FF').borderRadius(12).translate({x:this.offsetX}).animation({duration:1000,curve:Curve.FastOutSlowIn})FastOutSlowIn 的特点很明确:前段变化非常快(大概30%的时间内完成了70%的动画),后段变化非常慢(剩余70%的时间里慢慢完成最后30%的动画)。
这种曲线比 EaseOut 更"激进"。它起步更快,给人一种"立刻响应"的感觉,然后慢慢稳定下来。在 Material Design 中,这条曲线被称为"标准曲线",大量用于各种交互动画。
// 按钮点击反馈Button('确认').scale({x:this.btnScale,y:this.btnScale}).animation({duration:300,curve:Curve.FastOutSlowIn}).onMouseDown(()=>{this.btnScale=0.95}).onMouseUp(()=>{this.btnScale=1.0})FastOutSlowIn 特别适合弹性类动画的前半段。配合 setTimeout 做"弹出去再回来"的效果时,FastOutSlowIn 让弹出那一步非常干脆。
LinearOutSlowIn:平稳起步,柔和结束
Column().width(60).height(60).backgroundColor('#9B59B6').borderRadius(12).translate({x:this.offsetX}).animation({duration:1000,curve:Curve.LinearOutSlowIn})这条曲线前半段接近线性(匀速),后半段逐渐减速。可以理解为"正常速度走,然后慢慢停下来"。
它比 EaseOut 更"温和",因为起步不是那么冲。适合那些需要匀速运动一段再停下的场景,比如侧边栏的展开——先匀速推出,到位时缓缓停住。
// 侧边栏展开Column(){// 侧边栏内容}.translate({x:this.sideBarX}).animation({duration:400,curve:Curve.LinearOutSlowIn})FastOutLinearIn:快速启动,匀速结束
Column().width(60).height(60).backgroundColor('#FF6B9D').borderRadius(12).translate({x:this.offsetX}).animation({duration:1000,curve:Curve.FastOutLinearIn})和 LinearOutSlowIn 相反,这条曲线前段很快,后段变成线性。适合"快速响应然后保持速度离开"的场景,主要用于离场动画。
// 卡片向左滑出删除Column(){Text('待删除项')}.translate({x:this.cardX}).animation({duration:350,curve:Curve.FastOutLinearIn}).gesture(PanGesture({direction:PanDirection.Horizontal}).onActionEnd(()=>{this.cardX=-400// 快速飞出}))同场景对比:感受曲线差异
理论讲再多不如亲自看效果。我们来做一个对比实验:让同一个方块做同样的位移动画,只是换不同的曲线,直观感受差异。
@Entry@Componentstruct CurveCompareDemo{@Stateoffsets:Record<string,number>={'Linear':0,'EaseIn':0,'EaseOut':0,'EaseInOut':0,'FastOutSlowIn':0}privatecurves:Record<string,Curve>={'Linear':Curve.Linear,'EaseIn':Curve.EaseIn,'EaseOut':Curve.EaseOut,'EaseInOut':Curve.EaseInOut,'FastOutSlowIn':Curve.FastOutSlowIn}privatecolors:string[]=['#FF6B6B','#FFD93D','#4ECDC4','#6BCB77','#4D96FF']build(){Column(){Text('曲线对比').fontSize(18).fontWeight(FontWeight.Bold).margin({bottom:16})ForEach(Object.keys(this.offsets),(name:string,index:number)=>{Row(){Text(name).fontSize(11).fontColor('#666666').width(110)Stack({alignContent:Alignment.Start}){Column().width('100%').height(4).backgroundColor('#E8E8E8').borderRadius(2)Column().width(28).height(28).borderRadius(14).backgroundColor(this.colors[index]).translate({x:this.offsets[name],y:-12}).animation({duration:1200,curve:this.curves[name]})}.width('70%')}.width('100%').margin({bottom:16}).alignItems(VerticalAlign.Center)})Row({space:12}){Button('开始动画').onClick(()=>{Object.keys(this.offsets).forEach((name)=>{this.offsets[name]=200})})Button('重置').onClick(()=>{Object.keys(this.offsets).forEach((name)=>{this.offsets[name]=0})})}.width('100%').justifyContent(FlexAlign.Center).margin({top:16})}.width('100%').height('100%').backgroundColor('#F5F6FA').padding(20)}}点击"开始动画"后,五个圆球同时从左边出发到右边,但各自使用不同的曲线。你会清晰地看到:
- Linear 匀速前进,不快不慢
- EaseIn 起步很慢,后半段猛冲
- EaseOut 起步猛冲,慢慢停下
- EaseInOut 两头慢中间快
- FastOutSlowIn 起步最快,后段减速最明显
这个对比demo非常值得你亲手跑一遍。视觉上的差异比任何文字描述都直观。
曲线选择的决策指南
讲了这么多曲线,实际项目中怎么选?我总结了一套简单的决策逻辑:
元素进入场景
进场动画一律用减速类曲线:EaseOut、FastOutSlowIn、LinearOutSlowIn。
- 从屏幕外滑入:用 EaseOut(模拟减速停车)
- 从中心弹出:用 FastOutSlowIn(快速弹开,慢慢稳定)
- 淡入出现:用 EaseInOut(透明度过渡要柔和)
元素离开场景
离场动画用加速类曲线:EaseIn、FastOutLinearIn。
- 向屏幕外滑出:用 EaseIn(慢慢起步,加速飞走)
- 缩小消失:用 EaseIn(越来越快,直到看不见)
状态切换
在两个状态之间切换用对称类曲线:EaseInOut、Linear。
- 展开/折叠:用 EaseInOut(对称,不偏不倚)
- 颜色变化:用 EaseInOut(颜色过渡要平滑)
- 持续旋转/闪烁:用 Linear(保持恒定节奏)
交互反馈
用户操作的反馈用快速响应类曲线:FastOutSlowIn。
- 按钮点击:用 FastOutSlowIn(立刻响应,缓缓回位)
- 缩放弹跳:用 FastOutSlowIn(弹出干脆,稳定柔和)
- 拖拽释放:用 EaseOut(快速回位,慢慢停稳)
自定义曲线的进阶玩法
如果 Curve 枚举里的预设曲线都不能满足你的需求,HarmonyOS6 PC 还支持自定义贝塞尔曲线:
.animation({duration:500,curve:Curve.EaseOut// 使用预设})// 或者使用自定义三次贝塞尔曲线.animation({duration:500,curve:curves.cubicBezier(0.17,0.67,0.83,0.67)})贝塞尔曲线的四个参数定义了两个控制点的坐标。你可以用在线贝塞尔曲线编辑器(比如 cubic-bezier.com)来可视化地调整曲线形状,然后把参数值填进来。
还有一种更高级的 spring 弹簧曲线:
.animation({duration:500,curve:curves.springMotion(0.3,0.8)})弹簧曲线模拟真实的弹簧物理行为,第一个参数是阻尼比(越小弹得越厉害),第二个参数是刚度(越大弹得越快)。如果你想做iOS那种"果冻弹"的效果,弹簧曲线是你的最佳选择。
// iOS风格的弹性回弹Button('弹性按钮').scale({x:this.springScale,y:this.springScale}).animation({duration:800,curve:curves.springMotion(0.25,0.7)}).onMouseDown(()=>{this.springScale=0.85}).onMouseUp(()=>{this.springScale=1.0})阻尼比0.25意味着弹簧会来回弹几次才停下来,配合0.7的刚度,整个效果非常有弹性。这种效果在 HarmonyOS6 PC 端的卡片交互中特别好用。
常见误区
分享几个我自己踩过的坑:
误区一:所有动画都用同一个曲线。我见过有的项目全局用 EaseInOut,结果进场动画不够冲,离场动画不够快,点击反馈不够弹。不同场景需要不同曲线,别偷懒。
误区二:忽略时长和曲线的配合。FastOutSlowIn 配合200毫秒的时长,和配合800毫秒的时长,效果天差地别。短时长配合快曲线会很急促,长时长配合慢曲线会很优雅。要一起调。
误区三:动画曲线越复杂越好。不是的。一个简单的 EaseOut 配合合适的时长,往往比精心调制的弹簧曲线效果更好。动画的目的是服务用户体验,不是炫技。
误区四:Linear 是万能的。Linear 的适用范围其实很窄——只有需要"恒定速度"的场景才该用它。把 Linear 用在位移动画上,会让人觉得元素在"滑行"而不是"运动"。
曲线调参的实用技巧
调动画曲线是个经验活,但也有一些可量化的方法:
用手机录像功能录下你觉得"好用"的App的动画,然后逐帧回放,观察它的速度变化规律。你会发现大部分优秀App的动画都在用类似的曲线——起步快、结束慢,这就是 EaseOut 和 FastOutSlowIn 的领地。
在 HarmonyOS6 PC 上做开发时,给动画加一个调试模式:把 duration 放大到2000毫秒,这样你能清楚地看到曲线在每个阶段的速度变化。调好曲线后再把时长缩短到合理值。
把常用的曲线配置抽成常量,在项目中复用。比如定义一个ANIM_STANDARD代表标准曲线(FastOutSlowIn, 300ms),ANIM_ENTER代表进场曲线(EaseOut, 400ms),这样整个项目的动画风格会保持一致。
动画曲线是 HarmonyOS6 PC 开发中最容易被忽视、但影响最大的细节。希望这篇文章能帮你建立对曲线的直觉,下次写动画的时候,不再是无脑 Linear,而是胸有成竹地选出最合适的那一条。