万字干货 | HarmonyOS 6.0属性动画从入门到精通
在讲代码之前,咱们先聊个生活场景。想象你面前有一辆玩具小汽车,你要把它从桌子左边移到右边。你可以怎么做?方法一:直接拿起来放过去"啪"一下,车就到右边了。简单粗暴,但是——没有任何过程,突然就变了位置。方法二:用手推着它慢慢滑过去嗖~嗖~嗖~,车平稳地从左边滑到右边。你能看到整个移动过程,感觉就自然多了。在UI开发中:方法一就是直接修改属性——按钮位置从0变成100,没有中间状态方法二就是属性动画
开篇:老炮儿聊动画
大家好,我是小邢哥,一个写了14年代码的老程序员。
这些年,我见证了太多技术的起起落落——从jQuery一统天下,到React、Vue三分天下,从Android、iOS双雄争霸,到如今HarmonyOS异军突起。技术在变,但有些东西从来没变过,那就是用户体验永远是王道。
说到用户体验,就不得不提动画。
你有没有发现,同样是一个按钮点击效果,有的App让你感觉"嗯,丝滑",有的App让你觉得"卡巴,生硬"?这中间的差距,往往就差在那零点几秒的动画上。

我常跟团队里的年轻人说:代码是写给机器看的,但动画是写给人心看的。
今天,咱们就来把HarmonyOS 6.0的属性动画扒个底朝天。这篇文章,我会用14年的老炮儿视角,给你讲透:
-
属性动画是什么,怎么用
-
动画曲线背后的数学原理(放心,不讲公式)
-
各种动画效果的实战技巧
-
那些官方文档不会告诉你的坑
废话不多说,开整!
一、先搞懂:什么是属性动画?
1.1 从一个生活场景说起
在讲代码之前,咱们先聊个生活场景。
想象你面前有一辆玩具小汽车,你要把它从桌子左边移到右边。你可以怎么做?
方法一:直接拿起来放过去
"啪"一下,车就到右边了。简单粗暴,但是——没有任何过程,突然就变了位置。
方法二:用手推着它慢慢滑过去
嗖~嗖~嗖~,车平稳地从左边滑到右边。你能看到整个移动过程,感觉就自然多了。
在UI开发中:
-
方法一就是直接修改属性——按钮位置从0变成100,没有中间状态
-
方法二就是属性动画——按钮位置从0平滑过渡到100,有完整的中间过程
1.2 属性动画的本质
所谓属性动画,本质就是:
在一段时间内,让某个属性值从A平滑变化到B
这里有三个关键词:
-
时间:动画持续多久?500毫秒?1秒?
-
属性:要变的是啥?位置?大小?透明度?颜色?
-
平滑:怎么变?匀速?先快后慢?弹一弹?
举个例子:
把按钮的透明度,在300毫秒内,从1.0变成0.5
这就是一个最简单的属性动画定义。
1.3 属性动画 vs 帧动画
很多新手容易混淆这两个概念,我给你做个对比:
| 对比项 | 帧动画 | 属性动画 |
|---|---|---|
| 原理 | 快速播放多张图片 | 动态计算中间值 |
| 类比 | 翻页动画书 | 真人表演 |
| 灵活性 | 低(固定的图片序列) | 高(任意属性都能动) |
| 内存占用 | 高(需要多张图片) | 低(只存起止值) |
| 可交互性 | 差 | 好 |
属性动画的厉害之处在于:你只需要告诉系统起点和终点,中间的过程它自动帮你算。
这就像你跟出租车司机说"从这儿到人民广场",司机自己会选路线,你不用一步步指挥"向前100米,右转,再向前200米"。
1.4 HarmonyOS动画体系全景图
在正式开始写代码之前,咱们先看看HarmonyOS的动画体系长啥样:
HarmonyOS动画体系
├── 属性动画(今天的主角)
│ ├── 显式动画 animateTo
│ └── 属性动画 animation
├── 转场动画
│ ├── 页面间转场
│ └── 组件内转场
├── 路径动画
├── 粒子动画
└── 帧动画
今天咱们专攻属性动画,这是所有动画的基础,也是用得最多的。把这个吃透了,其他的触类旁通。
二、显式动画:animateTo
2.1 什么是显式动画?
HarmonyOS给我们提供了两种实现属性动画的方式:
-
显式动画:用
animateTo函数 -
属性动画:用
animation属性
先说显式动画 animateTo。
为什么叫"显式"?因为你要显式地调用一个函数来触发动画。就像你要开灯,得显式地去按一下开关。
2.2 基本语法
来,先看最简单的用法:
@Entry
@Component
struct AnimateToDemo {
@State boxWidth: number = 100
build() {
Column() {
// 一个会变化宽度的方块
Row()
.width(this.boxWidth)
.height(100)
.backgroundColor('#FF6B6B')
.borderRadius(10)
Button('点我变大')
.margin({ top: 20 })
.onClick(() => {
// 重点在这!显式动画来了!
animateTo({
duration: 500, // 动画时长500毫秒
}, () => {
this.boxWidth = 250 // 在闭包中修改状态
})
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
看到没?animateTo 接收两个参数:
-
动画配置对象:告诉系统动画怎么执行
-
闭包函数:在这里面修改状态变量
所有在闭包中被修改的状态变量,其引起的属性变化,都会以动画形式呈现。
这就是显式动画的核心思想:把状态修改"包裹"在animateTo里,让变化变成动画。
2.3 参数详解
animateTo 的配置对象可以设置很多参数,咱们逐个拆解:
animateTo({
duration: 500, // 动画时长(毫秒)
tempo: 1.0, // 动画速率,默认1.0
curve: Curve.EaseInOut, // 动画曲线(重点!后面专门讲)
delay: 0, // 延迟多久开始(毫秒)
iterations: 1, // 播放次数,-1表示无限循环
playMode: PlayMode.Normal, // 播放模式
onFinish: () => { // 动画结束回调
console.log('动画完成!')
}
}, () => {
// 状态变更
})
让我展开说说每个参数:
① duration - 持续时长
单位是毫秒。一般来说:
-
快速反馈:150-200ms(比如按钮点击效果)
-
普通过渡:300-500ms(比如页面元素移动)
-
强调效果:500-800ms(比如重要提示出现)
-
复杂动画:800ms以上
经验之谈:大部分场景300ms够用,别超过500ms,用户会觉得慢。
② tempo - 动画速率
这是个倍速播放器。
-
tempo = 1.0:正常速度
-
tempo = 2.0:快进2倍(实际时长减半)
-
tempo = 0.5:慢放一半(实际时长翻倍)
什么时候用?比如开发调试时,你想慢放看清动画细节,就把tempo设成0.2。
③ curve - 动画曲线
这个太重要了,我后面专门用一整章来讲。先记住它**决定了动画的"节奏感"**。
④ delay - 延迟启动
动画不是立刻开始,而是等一会儿再开始。
使用场景:做序列动画时,让多个动画错开执行。
// 第一个元素立刻开始
animateTo({ duration: 300 }, () => { this.scale1 = 1.2 })
// 第二个元素延迟100ms开始
animateTo({ duration: 300, delay: 100 }, () => { this.scale2 = 1.2 })
// 第三个元素延迟200ms开始
animateTo({ duration: 300, delay: 200 }, () => { this.scale3 = 1.2 })
这样就能做出"依次弹出"的效果,比一起动好看多了。
⑤ iterations - 播放次数
-
1:播放一次(默认)
-
3:播放三次
-
-1:无限循环
配合 playMode 使用,可以做出各种循环效果。
⑥ playMode - 播放模式
-
PlayMode.Normal:正常播放,每次从头开始 -
PlayMode.Reverse:反向播放 -
PlayMode.Alternate:交替播放(去—回—去—回...) -
PlayMode.AlternateReverse:反向交替播放
做呼吸灯效果用 Alternate 最合适:
animateTo({
duration: 1000,
iterations: -1,
playMode: PlayMode.Alternate,
curve: Curve.EaseInOut
}, () => {
this.opacity = this.opacity === 1 ? 0.3 : 1
})
这样透明度就会在1和0.3之间来回变化,完美的呼吸效果。
⑦ onFinish - 完成回调
动画结束时触发。注意几个细节:
-
如果
iterations = -1(无限循环),这个回调永远不会触发 -
如果动画被中途打断(比如新动画覆盖),也不会触发
2.4 实战案例:做一个点赞按钮
光说不练假把式,咱们来个实战:做一个有动画效果的点赞按钮。
@Entry
@Component
struct LikeButton {
@State isLiked: boolean = false
@State iconScale: number = 1
@State iconRotate: number = 0
build() {
Column() {
Image(this.isLiked ? $r('app.media.heart_filled') : $r('app.media.heart_outline'))
.width(50)
.height(50)
.scale({ x: this.iconScale, y: this.iconScale })
.rotate({ angle: this.iconRotate })
.onClick(() => {
this.handleLike()
})
Text(this.isLiked ? '已点赞' : '点赞')
.fontSize(14)
.fontColor(this.isLiked ? '#FF6B6B' : '#999999')
.margin({ top: 8 })
}
}
handleLike() {
if (!this.isLiked) {
// 点赞动画:先缩小再弹大,同时旋转
animateTo({
duration: 150,
curve: Curve.EaseIn
}, () => {
this.iconScale = 0.8
this.iconRotate = -15
})
// 延迟150ms后弹回来
setTimeout(() => {
animateTo({
duration: 300,
curve: Curve.EaseOut
}, () => {
this.iconScale = 1.2
this.iconRotate = 0
this.isLiked = true
})
// 最后回到正常大小
setTimeout(() => {
animateTo({
duration: 150,
curve: Curve.EaseOut
}, () => {
this.iconScale = 1
})
}, 300)
}, 150)
} else {
// 取消点赞:简单渐变
animateTo({
duration: 200,
curve: Curve.EaseOut
}, () => {
this.isLiked = false
})
}
}
}
这个点赞按钮的动画拆解:
-
按下去先缩小一点(反馈感)
-
然后弹出来变大(强调感)
-
最后恢复正常大小
三段式动画,让一个简单的点赞变得很有质感。
2.5 animateTo的进阶用法
① 同时改变多个属性
一个 animateTo 可以同时修改多个状态变量:
animateTo({ duration: 300 }, () => {
this.positionX = 200
this.positionY = 100
this.rotation = 45
this.scale = 1.2
this.opacity = 0.8
})
这些属性会同步变化,在同一时间完成动画。
② 串行动画(一个接一个)
用 onFinish 回调串起来:
animateTo({
duration: 300,
onFinish: () => {
// 第一段动画结束,开始第二段
animateTo({
duration: 300,
onFinish: () => {
// 第二段动画结束,开始第三段
animateTo({ duration: 300 }, () => {
this.step3State = true
})
}
}, () => {
this.step2State = true
})
}
}, () => {
this.step1State = true
})
不过说实话,这种回调套回调的写法有点"回调地狱"的味道。HarmonyOS也提供了更优雅的关键帧动画方案,后面会讲。
③ 并行动画(同时执行,但配置不同)
多次调用 animateTo,但修改不同的状态变量:
// 位置动画:300ms,缓入缓出
animateTo({
duration: 300,
curve: Curve.EaseInOut
}, () => {
this.positionX = 200
})
// 透明度动画:500ms,线性
animateTo({
duration: 500,
curve: Curve.Linear
}, () => {
this.opacity = 0.5
})
这样位置和透明度会同时开始变化,但用时和节奏各不相同。
2.6 animateTo的注意事项
老炮儿的经验之谈,踩过的坑你别再踩:
坑1:闭包里不能做状态无关的操作
// ❌ 错误示范
animateTo({ duration: 300 }, () => {
this.width = 200
console.log('这行代码会立刻执行!') // 不是动画结束才执行
this.doSomething() // 这也是立刻执行
})
闭包里的代码是立刻执行的,不是动画结束才执行!想在动画结束后做事情,用 onFinish。
坑2:频繁调用animateTo会相互打断
// 快速点击多次
onClick(() => {
animateTo({ duration: 300 }, () => {
this.scale = 1.2
})
})
如果用户快速点击,新动画会打断旧动画,可能造成状态混乱。解决方案:加个标志位判断,或者使用防抖。
坑3:并非所有属性都能动画
能做属性动画的有:
-
位置:
position、offset -
大小:
width、height -
变换:
scale、rotate、translate -
透明度:
opacity -
背景色:
backgroundColor -
其他数值型属性
不能做动画的:
-
visibility(要么显示要么隐藏,没有中间状态) -
display -
一些文本属性
三、属性动画:animation
3.1 什么是animation?
除了 animateTo,HarmonyOS还提供了另一种属性动画方式:animation 属性。
Row()
.width(this.boxWidth)
.height(100)
.animation({ // 注意:这是个属性,不是函数
duration: 300,
curve: Curve.EaseOut
})
这样写之后,只要 this.boxWidth 变化,就会自动以动画形式过渡,不需要手动调用 animateTo。
3.2 animation vs animateTo
这两种方式有什么区别?老炮儿给你画个表:
| 对比项 | animateTo | animation |
|---|---|---|
| 调用方式 | 函数调用(显式) | 属性设置(隐式) |
| 控制粒度 | 每次动画可以不同配置 | 固定配置,统一生效 |
| 适用场景 | 复杂动画,需要精确控制 | 简单动画,状态变就自动动 |
| 代码量 | 稍多 | 更简洁 |
| 动画取消 | 不好取消 | 设置 animation(null) |
什么时候用 animateTo?
-
需要在动画中执行多个不同配置
-
需要监听动画完成事件
-
需要做序列动画
-
需要精确控制动画时机
什么时候用 animation?
-
简单的状态变化动画
-
希望"设置一次,处处生效"
-
代码简洁优先
3.3 animation的详细用法
@Entry
@Component
struct AnimationDemo {
@State boxSize: number = 100
@State boxColor: string = '#FF6B6B'
@State boxRadius: number = 10
build() {
Column() {
Row()
.width(this.boxSize)
.height(this.boxSize)
.backgroundColor(this.boxColor)
.borderRadius(this.boxRadius)
// 关键:给这些属性加上动画
.animation({
duration: 400,
curve: Curve.EaseInOut,
delay: 0,
iterations: 1,
playMode: PlayMode.Normal
})
Row() {
Button('变大')
.onClick(() => {
this.boxSize = 150 // 直接改状态,自动动画
})
Button('变色')
.margin({ left: 10 })
.onClick(() => {
this.boxColor = '#4ECDC4' // 直接改状态,自动动画
})
Button('圆角')
.margin({ left: 10 })
.onClick(() => {
this.boxRadius = 75 // 直接改状态,自动动画
})
}
.margin({ top: 20 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
看到没?用了 animation 之后,代码变得很干净。不需要每次都包裹 animateTo,状态一变,自动就动画过渡了。
3.4 animation的参数
animation 的参数和 animateTo 基本一致:
.animation({
duration: 300, // 时长
tempo: 1.0, // 速率
curve: Curve.EaseInOut, // 曲线
delay: 0, // 延迟
iterations: 1, // 次数
playMode: PlayMode.Normal, // 播放模式
onFinish: () => {} // 完成回调
})
3.5 animation的生效范围
重点来了!animation 只对它上面的属性生效。
Row()
.width(this.width) // ✅ 会有动画
.height(this.height) // ✅ 会有动画
.animation({ duration: 300 }) // animation在这里
.backgroundColor(this.color) // ❌ 不会有动画!
.opacity(this.opacity) // ❌ 不会有动画!
所以,animation要放在需要动画的属性后面。
如果你想所有属性都有动画,就把 animation 放最后:
Row()
.width(this.width)
.height(this.height)
.backgroundColor(this.color)
.opacity(this.opacity)
.animation({ duration: 300 }) // 放最后,上面的都有动画
3.6 多个animation的情况
如果你想不同属性用不同的动画配置,可以放多个 animation:
Row()
.width(this.width)
.animation({ duration: 200, curve: Curve.EaseOut }) // 宽度:快速
.height(this.height)
.animation({ duration: 500, curve: Curve.Spring }) // 高度:弹簧
.backgroundColor(this.color)
.animation({ duration: 1000, curve: Curve.Linear }) // 颜色:缓慢线性
这样每组属性就有自己独立的动画配置了。
3.7 禁用动画
有时候你想临时禁用动画,可以传 null 或者 undefined:
Row()
.width(this.width)
.animation(this.enableAnimation ? { duration: 300 } : null)
或者设置 duration 为 0:
.animation({ duration: 0 }) // 等于没有动画
3.8 实战案例:卡片展开效果
@Entry
@Component
struct CardExpand {
@State isExpanded: boolean = false
build() {
Column() {
Column() {
// 卡片头部
Row() {
Text('订单详情')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Blank()
Image($r('app.media.arrow_down'))
.width(20)
.height(20)
.rotate({ angle: this.isExpanded ? 180 : 0 })
.animation({ duration: 300, curve: Curve.EaseOut })
}
.width('100%')
.padding(16)
.onClick(() => {
this.isExpanded = !this.isExpanded
})
// 卡片内容(可展开收起)
if (this.isExpanded) {
Column() {
this.InfoRow('订单编号', '202412040001')
this.InfoRow('商品名称', 'HarmonyOS手机壳')
this.InfoRow('订单金额', '¥99.00')
this.InfoRow('收货地址', '北京市朝阳区...')
}
.padding(16)
.opacity(this.isExpanded ? 1 : 0)
.animation({ duration: 300, curve: Curve.EaseOut })
}
}
.width('90%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 10,
color: 'rgba(0,0,0,0.1)',
offsetX: 0,
offsetY: 2
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.padding({ top: 50 })
}
@Builder
InfoRow(label: string, value: string) {
Row() {
Text(label)
.fontSize(14)
.fontColor('#999999')
Blank()
Text(value)
.fontSize(14)
.fontColor('#333333')
}
.width('100%')
.margin({ bottom: 8 })
}
}
这个案例展示了 animation 的便捷性:箭头图标的旋转、内容区的透明度变化,都是自动动画的,代码很清爽。
四、动画曲线:灵魂所在
4.1 什么是动画曲线?
终于到了重头戏——动画曲线(Animation Curve)。
如果说动画时长决定了"动画多久完成",那么动画曲线就决定了"动画怎么完成"。
还是用生活场景来理解:
假设你要从A点走到B点,用时10秒。
-
匀速:每秒走相同的距离,稳稳当当
-
先慢后快:开始慢悠悠,后来越跑越快
-
先快后慢:一开始冲刺,快到了就减速
-
弹簧效果:到了终点还要弹一下,再稳定下来
这就是不同的"运动曲线",在动画领域,我们叫它动画曲线或缓动函数。
4.2 为什么动画曲线重要?
让我举个真实的例子。
同样是一个弹窗出现动画,持续300毫秒:
用线性曲线(Linear):
-
弹窗匀速从小变大
-
感觉:机械、生硬、像机器人
用缓出曲线(EaseOut):
-
弹窗快速变大,到最后慢慢减速
-
感觉:自然、舒适、像真实物体
用弹簧曲线(Spring):
-
弹窗快速变大,超过目标大小一点点,再弹回来
-
感觉:活泼、有弹性、像皮球
同样的时长,不同的曲线,用户感受天差地别。
这就是为什么我说:动画曲线是动画的灵魂。
4.3 HarmonyOS内置曲线详解
HarmonyOS提供了一系列预设曲线,都在 Curve 枚举里:
① Curve.Linear - 线性
curve: Curve.Linear
最简单的曲线,匀速变化。
适用场景:
-
进度条
-
需要表现"机械感"的场景
-
对时间精确同步的场景(比如和音乐节奏同步)
不推荐用于:
-
大部分UI动画(太生硬)
② Curve.Ease - 标准缓动
curve: Curve.Ease
开始和结束都有点慢,中间快一些。经典的"慢-快-慢"节奏。
适用场景:
-
通用的UI动画
-
不知道用什么曲线时的默认选择
③ Curve.EaseIn - 缓入
curve: Curve.EaseIn
开始慢,越来越快。像东西从静止开始加速。
适用场景:
-
元素离开画面(飞出去)
-
需要"蓄力"感觉的动画
④ Curve.EaseOut - 缓出
curve: Curve.EaseOut
开始快,越来越慢。像东西飞过来,快到了减速。
适用场景:
-
元素进入画面
-
弹窗出现
-
抽屉展开
-
最常用的曲线,没有之一
⑤ Curve.EaseInOut - 缓入缓出
curve: Curve.EaseInOut
开始慢,中间快,结束慢。完整的加速-减速过程。
适用场景:
-
元素在场景内移动(从A点到B点)
-
循环动画
-
需要"自然"感觉的大多数场景
⑥ Curve.FastOutSlowIn - 快出慢入
curve: Curve.FastOutSlowIn
Material Design推荐的标准曲线,特点是一开始就很快,然后缓慢减速。
适用场景:
-
安卓风格的UI
-
响应用户操作的动画
⑦ Curve.LinearOutSlowIn - 线出慢入
curve: Curve.LinearOutSlowIn
开始线性加速,后半段慢慢减速。用于进入场景的动画。
8 Curve.FastOutLinearIn - 快出线入
curve: Curve.FastOutLinearIn
一开始就快,然后保持线性到结束。用于离开场景的动画。
⑨ Curve.ExtremeDeceleration - 极速减速
curve: Curve.ExtremeDeceleration
非常激进的减速曲线,一开始就很快,然后迅速减到几乎停止。
适用场景:
-
强调"迅速响应"的交互
-
短时间动画
⑩ Curve.Sharp - 锐利
curve: Curve.Sharp
锐利的曲线,快速启动和快速结束。
⑪ Curve.Smooth - 平滑
curve: Curve.Smooth
非常平滑的曲线,过渡自然。
4.4 曲线选择速查表
用了14年,我总结出一套"曲线选择经验":
| 场景 | 推荐曲线 | 原因 |
|---|---|---|
| 弹窗出现 | EaseOut | 快速响应,自然减速 |
| 弹窗消失 | EaseIn | 加速离开 |
| 元素移动 | EaseInOut | 自然的加速减速 |
| 按钮反馈 | EaseOut | 快速响应 |
| 加载动画(循环) | EaseInOut | 平滑循环 |
| 列表项出现 | EaseOut | 快速呈现 |
| 下拉刷新 | Spring | 弹性感 |
| 开关切换 | Spring | 物理感 |
4.5 弹簧曲线:让动画有物理感
前面提到的曲线都是"贝塞尔曲线",特点是给定时间必定结束。
而弹簧曲线不一样,它模拟的是真实世界的弹簧物理运动,效果更自然。
HarmonyOS提供了三种弹簧曲线:
① Curve.Spring - 基础弹簧
curve: Curve.Spring
会有一个"过冲"效果——超过目标值再弹回来。
② curves.springMotion() - 弹簧运动曲线
import { curves } from '@kit.ArkUI'
animateTo({
curve: curves.springMotion(0.5, 0.8)
}, () => {
this.scale = 1.2
})
参数说明:
-
第一个参数:响应时间(0-1),越小越快
-
第二个参数:阻尼比(0-1),越小弹得越多
③ curves.responsiveSpringMotion() - 响应式弹簧
curve: curves.responsiveSpringMotion(0.35, 0.9)
这个是专门为跟手动画设计的。什么是跟手动画?比如:
-
拖动卡片
-
滑动开关
-
下拉刷新时的弹性效果
特点是响应速度更快,适合持续交互。
④ 自定义弹簧曲线 curves.springCurve()
curve: curves.springCurve(100, 1, 228, 30)
四个参数分别是:
-
velocity:初始速度
-
mass:质量(越大越沉)
-
stiffness:刚度(越大弹得越快)
-
damping:阻尼(越大弹得越少)
这就像物理模拟,你可以调出各种弹簧效果:
-
轻盈的小弹簧:小质量,高刚度
-
沉稳的大弹簧:大质量,高阻尼
4.6 自定义贝塞尔曲线
如果预设曲线都不满足需求,你还可以自定义:
import { curves } from '@kit.ArkUI'
animateTo({
curve: curves.cubicBezierCurve(0.25, 0.1, 0.25, 1.0)
}, () => {
this.positionX = 200
})
cubicBezierCurve(x1, y1, x2, y2) 用四个参数定义一条三次贝塞尔曲线。
这四个数字是两个控制点的坐标:
-
(x1, y1):第一个控制点
-
(x2, y2):第二个控制点
如果你不太懂贝塞尔曲线的原理,没关系,推荐你一个神器:
这个网站可以可视化地调整曲线,调好了直接复制参数。
常见的一些贝塞尔曲线值:
-
Ease:(0.25, 0.1, 0.25, 1.0)
-
EaseIn:(0.42, 0, 1, 1)
-
EaseOut:(0, 0, 0.58, 1)
-
EaseInOut:(0.42, 0, 0.58, 1)
4.7 曲线对比实战
说了这么多,咱们来个实际的对比demo:
import { curves } from '@kit.ArkUI'
@Entry
@Component
struct CurveComparison {
@State linear: number = 0
@State ease: number = 0
@State easeOut: number = 0
@State spring: number = 0
build() {
Column() {
// 4条轨道,4种曲线
this.TrackRow('Linear', this.linear, '#FF6B6B')
this.TrackRow('Ease', this.ease, '#4ECDC4')
this.TrackRow('EaseOut', this.easeOut, '#45B7D1')
this.TrackRow('Spring', this.spring, '#96CEB4')
Button('开始对比')
.margin({ top: 30 })
.onClick(() => {
// 重置
this.linear = 0
this.ease = 0
this.easeOut = 0
this.spring = 0
// 延迟一点点再开始,让重置生效
setTimeout(() => {
animateTo({ duration: 1000, curve: Curve.Linear }, () => {
this.linear = 1
})
animateTo({ duration: 1000, curve: Curve.Ease }, () => {
this.ease = 1
})
animateTo({ duration: 1000, curve: Curve.EaseOut }, () => {
this.easeOut = 1
})
animateTo({ duration: 1000, curve: curves.springMotion(0.3, 0.8) }, () => {
this.spring = 1
})
}, 50)
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(20)
}
@Builder
TrackRow(label: string, progress: number, color: string) {
Column() {
Text(label)
.fontSize(12)
.fontColor('#666666')
.margin({ bottom: 5 })
Stack({ alignContent: Alignment.Start }) {
// 轨道
Row()
.width('100%')
.height(4)
.backgroundColor('#EEEEEE')
.borderRadius(2)
// 小球
Row()
.width(30)
.height(30)
.backgroundColor(color)
.borderRadius(15)
.margin({ left: `${progress * 85}%` }) // 移动位置
}
.width('100%')
}
.margin({ bottom: 20 })
}
}
运行这个demo,点击按钮,你会看到4个小球同时出发,但运动轨迹完全不同:
-
Linear:匀速直线
-
Ease:慢-快-慢
-
EaseOut:快-慢
-
Spring:会超过终点再弹回来
这就是曲线的力量。
五、动画效果进阶
5.1 关键帧动画
前面我们讲的动画,都是从A到B的单一变化。如果我想做更复杂的动画,比如:
A → B → C → D
怎么办?
HarmonyOS提供了关键帧动画 keyframeAnimateTo。
import { keyframeAnimateTo, KeyframeState } from '@kit.ArkUI'
@Entry
@Component
struct KeyframeDemo {
@State myScale: number = 1
@State myRotate: number = 0
build() {
Column() {
Row()
.width(100)
.height(100)
.backgroundColor('#FF6B6B')
.scale({ x: this.myScale, y: this.myScale })
.rotate({ angle: this.myRotate })
Button('关键帧动画')
.margin({ top: 30 })
.onClick(() => {
this.runKeyframeAnimation()
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
runKeyframeAnimation() {
keyframeAnimateTo({
iterations: 1,
}, [
// 第一段:0-30%时间,缩小并旋转
{
duration: 300,
curve: Curve.EaseIn,
event: () => {
this.myScale = 0.8
this.myRotate = 45
}
},
// 第二段:30%-70%时间,放大并继续旋转
{
duration: 400,
curve: Curve.EaseOut,
event: () => {
this.myScale = 1.3
this.myRotate = 180
}
},
// 第三段:70%-100%时间,恢复原状
{
duration: 300,
curve: Curve.EaseInOut,
event: () => {
this.myScale = 1
this.myRotate = 360
}
}
])
}
}
关键帧动画的核心是:把动画拆成多个阶段,每个阶段有自己的时长、曲线和目标值。
这比用 setTimeout + animateTo 串起来优雅多了。
5.2 组件内转场动画
除了属性动画,HarmonyOS还有转场动画,用于组件出现/消失时的效果。
@Entry
@Component
struct TransitionDemo {
@State isShow: boolean = false
build() {
Column() {
Button(this.isShow ? '隐藏' : '显示')
.onClick(() => {
this.isShow = !this.isShow
})
if (this.isShow) {
Row()
.width(200)
.height(200)
.backgroundColor('#FF6B6B')
.borderRadius(20)
.margin({ top: 20 })
// 组件出现的转场效果
.transition(TransitionEffect.OPACITY.animation({
duration: 300,
curve: Curve.EaseOut
}))
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
TransitionEffect 有很多预设效果:
-
OPACITY:透明度渐变 -
SCALE:缩放 -
SLIDE:滑动 -
MOVE:移动 -
ASYMMETRIC:非对称(出现和消失不同效果)
还可以组合使用:
.transition(
TransitionEffect.OPACITY
.combine(TransitionEffect.scale({ x: 0.5, y: 0.5 }))
.animation({ duration: 300, curve: Curve.EaseOut })
)
5.3 共享元素转场
这是一个特别酷的效果:两个页面间,某个元素平滑过渡。
比如列表页点击一张图片,图片"飞到"详情页变成大图,而不是直接跳转。
// 列表页
Image($r('app.media.photo'))
.width(100)
.height(100)
.sharedTransition('photoId', {
duration: 300,
curve: Curve.EaseInOut
})
.onClick(() => {
router.pushUrl({ url: 'pages/Detail' })
})
// 详情页
Image($r('app.media.photo'))
.width('100%')
.height(300)
.sharedTransition('photoId', { // 相同的id
duration: 300,
curve: Curve.EaseInOut
})
关键是两个页面的元素用相同的 sharedTransition id,系统就会自动计算过渡动画。
5.4 路径动画
让元素沿着特定路径运动:
@Entry
@Component
struct PathAnimationDemo {
@State posX: number = 0
@State posY: number = 0
build() {
Column() {
Stack() {
// 画一条参考路径
Path()
.width(300)
.height(200)
.commands('M0 100 Q150 0 300 100') // 二次贝塞尔曲线
.stroke('#CCCCCC')
.strokeWidth(2)
.fill('none')
// 运动的小球
Row()
.width(20)
.height(20)
.backgroundColor('#FF6B6B')
.borderRadius(10)
.position({ x: this.posX, y: this.posY })
}
.width(300)
.height(200)
Button('开始')
.margin({ top: 30 })
.onClick(() => {
// 使用motionPath实现路径动画
animateTo({
duration: 2000,
curve: Curve.Linear,
iterations: -1
}, () => {
// 通过计算贝塞尔曲线上的点来实现
// 实际开发中可以使用motionPath属性
})
})
}
}
}
路径动画的完整实现需要使用 motionPath 属性,可以指定SVG路径字符串。
5.5 粒子动画
HarmonyOS还支持粒子动画,用于做一些炫酷的效果:
@Entry
@Component
struct ParticleDemo {
build() {
Stack() {
Particle({
particles: [{
emitter: {
particle: {
type: ParticleType.POINT,
config: { radius: 5 },
count: 100
}
}
}]
})
.width('100%')
.height('100%')
}
}
}
粒子动画可以用来做:
-
庆祝撒花效果
-
雪花飘落
-
火焰燃烧
-
点赞爆发
六、性能优化与最佳实践
6.1 动画性能的关键指标
作为老炮儿,不得不提性能。
动画卡不卡,关键看两个指标:
① 帧率(FPS)
理想情况下,动画应该保持在60FPS,即每秒刷新60次。
-
60FPS:丝滑
-
30-60FPS:还行
-
<30FPS:明显卡顿
② 掉帧(Jank)
即使平均帧率不错,如果中途突然卡一下(掉帧),用户也会觉得卡。
6.2 影响动画性能的因素
① 动画属性的选择
不同属性的动画性能差距很大:
| 性能好 | 性能中等 | 性能差 |
|---|---|---|
| transform(scale, rotate, translate) | opacity | width, height |
| opacity | backgroundColor | padding, margin |
原因是:
-
transform和opacity可以由GPU直接处理,不需要重新布局 -
width、height等需要重新计算布局,然后重新绘制
所以,能用 transform 实现的效果,就不要用 width/height。
// ❌ 不推荐:用width/height实现缩放
Row()
.width(this.size)
.height(this.size)
// ✅ 推荐:用scale实现缩放
Row()
.width(100)
.height(100)
.scale({ x: this.scale, y: this.scale })
② 同时动画的元素数量
一次性让100个元素同时动画,肯定比1个元素动画更耗性能。
解决方案:
-
使用
delay错开启动时间 -
复用动画组件
-
使用虚拟列表(只渲染可见区域)
③ 动画时长
太长的动画会持续占用资源。
建议大部分动画控制在300-500ms以内。
6.3 动画调试技巧
① 慢放动画
设置 tempo: 0.1 可以把动画慢放10倍,方便观察细节:
animateTo({
duration: 300,
tempo: 0.1 // 慢放10倍
}, () => {
this.scale = 1.2
})
② 使用DevEco Studio的性能分析工具
DevEco Studio提供了帧率监控、渲染耗时分析等工具,可以帮你定位性能问题。
③ 简化测试
怀疑某个动画有问题,就把它单独拿出来测试,排除干扰因素。
6.4 常见坑点总结
坑1:动画回调里调用动画
// ❌ 可能出问题
animateTo({
onFinish: () => {
animateTo({...}, () => {...}) // 嵌套调用
}
}, () => {...})
虽然语法上没问题,但嵌套太深会导致逻辑混乱,建议用关键帧动画代替。
坑2:在循环里创建动画
// ❌ 性能差
for (let i = 0; i < 100; i++) {
animateTo({ duration: 300 }, () => {
this.items[i].scale = 1.2
})
}
这样会创建100个独立的动画,性能爆炸。
正确做法是在一个 animateTo 里修改所有状态:
// ✅ 性能好
animateTo({ duration: 300 }, () => {
for (let i = 0; i < 100; i++) {
this.items[i].scale = 1.2
}
})
坑3:忘记处理动画中断
用户可能在动画播放中途执行其他操作,导致状态不一致。
// 加个标志位
@State isAnimating: boolean = false
handleClick() {
if (this.isAnimating) return
this.isAnimating = true
animateTo({
duration: 300,
onFinish: () => {
this.isAnimating = false
}
}, () => {...})
}
坑4:animation放错位置
// ❌ backgroundColor不会有动画
Row()
.animation({ duration: 300 })
.backgroundColor(this.color)
// ✅ backgroundColor会有动画
Row()
.backgroundColor(this.color)
.animation({ duration: 300 })
记住:animation 只对它前面的属性生效!
6.5 最佳实践清单
总结一下做动画的最佳实践:
-
优先使用 transform(scale, rotate, translate)和 opacity,它们性能最好
-
动画时长控制在300-500ms,太短感觉不到,太长觉得慢
-
进入用 EaseOut,退出用 EaseIn,移动用 EaseInOut,这是最安全的选择
-
需要物理感时用弹簧曲线,特别是跟手操作
-
复杂动画用关键帧,不要callback套callback
-
animation放在属性后面,别忘了作用范围
-
同时动画的元素不要太多,必要时用delay错开
-
加防抖/标志位,防止动画被频繁触发打断
-
开发时用tempo慢放,方便调试
-
上线前测试真机性能,模拟器不准
七、实战项目:做一个抽屉菜单
光说不练假把式,咱们来个完整的实战项目:带动画的抽屉菜单。
7.1 需求分析
-
点击按钮,抽屉从左边滑出
-
抽屉出现时,背景变暗
-
点击遮罩层,抽屉收回
-
抽屉内的菜单项依次出现(错开动画)
7.2 完整代码
import { curves } from '@kit.ArkUI'
@Entry
@Component
struct DrawerMenu {
@State isOpen: boolean = false
@State drawerTranslateX: number = -280 // 抽屉宽度280
@State maskOpacity: number = 0
@State menuItemsVisible: boolean[] = [false, false, false, false, false]
private menuItems: string[] = ['首页', '个人中心', '设置', '关于我们', '退出登录']
build() {
Stack() {
// 主内容区
Column() {
Row() {
Button('☰')
.fontSize(24)
.backgroundColor('transparent')
.fontColor('#333333')
.onClick(() => {
this.openDrawer()
})
Text('我的应用')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ left: 15 })
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor('#FFFFFF')
.shadow({
radius: 4,
color: 'rgba(0,0,0,0.1)',
offsetX: 0,
offsetY: 2
})
// 页面内容
Column() {
Text('这里是主要内容区域')
.fontSize(16)
.fontColor('#666666')
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
// 遮罩层
if (this.maskOpacity > 0) {
Row()
.width('100%')
.height('100%')
.backgroundColor(`rgba(0, 0, 0, ${this.maskOpacity * 0.5})`)
.onClick(() => {
this.closeDrawer()
})
}
// 抽屉
Column() {
// 抽屉头部
Column() {
Image($r('app.media.startIcon'))
.width(80)
.height(80)
.borderRadius(40)
Text('小邢哥')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 12 })
Text('14年编程老炮儿')
.fontSize(14)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: 60, bottom: 30 })
.backgroundColor('#FF6B6B')
.alignItems(HorizontalAlign.Center)
// 菜单项
Column() {
ForEach(this.menuItems, (item: string, index: number) => {
Row() {
// Text(this.getMenuIcon(index))
SymbolGlyph(this.getMenuIcon(index))
.fontSize(20)
.width(30)
Text(item)
.fontSize(16)
.fontColor('#333333')
.margin({ left: 12 })
}
.width('100%')
.height(56)
.padding({ left: 20 })
.backgroundColor(this.menuItemsVisible[index] ? '#FFFFFF' : '#F5F5F5')
.opacity(this.menuItemsVisible[index] ? 1 : 0)
.translate({ x: this.menuItemsVisible[index] ? 0 : -50 })
.animation({
duration: 300,
curve: Curve.EaseOut
})
.onClick(() => {
console.log(`点击了:${item}`)
this.closeDrawer()
})
})
}
.width('100%')
.padding({ top: 10 })
}
.width(280)
.height('100%')
.backgroundColor('#FFFFFF')
.position({ x: 0, y: 0 })
.translate({ x: this.drawerTranslateX })
}
.width('100%')
.height('100%')
}
getMenuIcon(index: number): Resource {
const icons = [$r("sys.symbol.home_key"),$r("sys.symbol.school"),
$r("sys.symbol.house_setting"), $r("sys.symbol.message"),$r("sys.symbol.star")]
return icons[index]
}
openDrawer() {
// 抽屉滑入 + 遮罩出现
animateTo({
duration: 300,
curve: curves.springMotion(0.4, 0.9)
}, () => {
this.drawerTranslateX = 0
this.maskOpacity = 1
this.isOpen = true
})
// 菜单项依次出现
this.menuItemsVisible.forEach((_, index) => {
setTimeout(() => {
this.menuItemsVisible[index] = true
}, 100 + index * 60) // 每个菜单项延迟60ms
})
}
closeDrawer() {
// 先隐藏菜单项
this.menuItemsVisible = [false, false, false, false, false]
// 抽屉滑出 + 遮罩消失
animateTo({
duration: 250,
curve: Curve.EaseIn
}, () => {
this.drawerTranslateX = -280
this.maskOpacity = 0
this.isOpen = false
})
}
}


7.3 动画拆解
这个demo用到了几个动画技巧:
-
弹簧曲线开启抽屉:让抽屉有"弹出"的感觉
-
普通曲线关闭抽屉:快速收起,不需要弹簧
-
序列动画:菜单项依次出现,使用setTimeout错开
-
组合动画:抽屉滑动 + 遮罩渐变同时进行
这就是一个"麻雀虽小五脏俱全"的动画案例。
八、总结
8.1 知识点回顾
写到这儿,咱们把HarmonyOS 6.0属性动画的核心知识点都过了一遍:
属性动画基础
-
属性动画的本质:在一段时间内平滑改变属性值
-
两种实现方式:animateTo(显式)和 animation(属性)
animateTo显式动画
-
语法:
animateTo(config, closure) -
参数:duration、curve、delay、iterations、playMode、onFinish
-
闭包中的状态变更会以动画形式呈现
animation属性动画
-
语法:
.animation(config) -
作用范围:只对它前面的属性生效
-
更简洁,适合简单场景
动画曲线
-
线性曲线:Linear,匀速变化
-
缓动曲线:Ease、EaseIn、EaseOut、EaseInOut
-
弹簧曲线:Spring、springMotion、responsiveSpringMotion
-
自定义曲线:cubicBezierCurve
进阶动画
-
关键帧动画:keyframeAnimateTo
-
转场动画:transition
-
共享元素转场:sharedTransition
性能优化
-
优先使用transform和opacity
-
控制动画时长和数量
-
使用防抖和标志位
8.2 老炮儿的忠告
最后,作为一个写了14年代码的老程序员,我想说几句真心话:
1. 动画不是越多越好
很多新人觉得动画炫酷,就往里面堆。结果整个App花里胡哨,用户眼花缭乱。
记住:动画是为用户体验服务的,不是为了炫技。
该静的时候静,该动的时候动。克制,是高级。
2. 先学会用,再研究原理
这篇文章我讲了很多原理性的东西,但你不需要一开始就全搞懂。
先把 animateTo 和 animation 用熟,做几个项目,遇到问题再深究。实践出真知。
3. 多抄,多改,多练
动画这东西,光看文档是学不会的。你得动手做。
找几个你觉得动画做得好的App,模仿它的效果。先抄,再改,最后创新。
4. 保持好奇心
技术永远在变,今天的HarmonyOS,明天可能又有新东西出来。
保持学习的热情,保持对新技术的好奇心。这才是一个程序员最重要的品质。
好了,这篇一万多字的文章就写到这儿。
如果你觉得有帮助,点个赞,转发一下,让更多人看到。
我是小邢哥,14年编程老炮,下期见!
更多推荐



所有评论(0)