你写的 ArkUI 动画,真的是“丝滑”,还是在偷偷“掉帧求饶”?
本文分享了鸿蒙开发中动画实现与优化的实用技巧。首先区分了属性动画和转场动画的不同应用场景,介绍了隐式动画和显式动画的实现方式及注意事项。重点讲解了性能优化策略,包括优先使用合成属性、限制动画范围、避免主线程阻塞等。最后通过一个点赞按钮的实战案例,展示了如何组合多种动画效果(缩放、旋转、粒子效果)实现复杂交互。文章强调动画设计要兼顾流畅性和性能,避免过度炫技导致体验下降,为鸿蒙开发者提供了可落地的动
👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!🚀
前言:动画不是“炫技”,是让用户觉得“这个 App 有灵魂”✨
我见过太多动画翻车现场:
- 点一下按钮,界面“闪一下”就结束了(像没睡醒😪)
- 列表滑动时还在做大范围重排动画(帧率当场跪下🧎)
- 进场/退场动画写得像随机抽奖(用户都不敢点返回🙃)
你要记住一句大实话:
动画是“性能预算”里最容易超支的一项。
你越想“华丽”,越要“克制”。
1)属性动画 vs 转场动画:别混着用,不然你自己都绕晕
1.1 属性动画(Property Animation):让“某个属性”随时间变化
典型场景:
- 按钮按下缩放(scale)
- 透明度渐变(opacity)
- 位移(translate)
- 旋转(rotate)
- 颜色渐变(color)
- 高度/宽度变化(⚠️这个会牵扯布局,慎用)
✅ 优点:细粒度控制、交互反馈很爽
⚠️ 缺点:你一旦动画了“影响布局的属性”(比如 height/width/padding/margin),就可能触发频繁测量与布局,掉帧风险直线上升😤
1.2 转场动画(Transition):让“组件出现/消失/切换”更自然
典型场景:
- 弹窗进出
- 列表项插入/删除
- 页面区域切换(加载态↔内容态)
- 折叠面板展开/收起时内容进出
✅ 优点:表达结构变化更自然,用户不迷路
⚠️ 缺点:写不好会出现“突然闪现/突然消失”,像鬼片👻
2)Animation / Transition 使用:两条路线,别用错姿势
我这里用 ArkUI 常见的两类方式来讲:隐式动画(给属性挂 animation) 和 显式动画(状态更新包在 animateTo)。
写法在不同版本/模板会有细微差异,但思路完全通用。
2.1 隐式属性动画:最省心,但要管住手😅
隐式动画的核心就是:属性变了,框架替你补中间帧。
@Entry
@Component
struct ImplicitAnimDemo {
@State pressed: boolean = false
build() {
Column({ space: 16 }) {
Text('隐式属性动画:按下会缩放+变淡')
Button(this.pressed ? '松手~' : '按我一下')
.scale({ x: this.pressed ? 0.96 : 1, y: this.pressed ? 0.96 : 1 })
.opacity(this.pressed ? 0.75 : 1)
.animation({
duration: 180,
curve: Curve.EaseOut
})
.onTouch((e) => {
if (e.type === TouchType.Down) this.pressed = true
if (e.type === TouchType.Up || e.type === TouchType.Cancel) this.pressed = false
})
}
.padding(16)
}
}
我个人的“暴躁提醒”😤:
- 隐式动画别挂到“整个大容器”上(容易把不该动的也动了)
- 别在列表项里对布局属性搞隐式动画(你会看到掉帧在向你招手👋)
2.2 显式属性动画:把“变化”包进动画块,更可控
显式动画适合:
- 你想明确控制某次状态变化“必须动画”
- 或想同一时刻协调多个属性一起动
@Entry
@Component
struct ExplicitAnimDemo {
@State expanded: boolean = false
build() {
Column({ space: 12 }) {
Text('显式动画:卡片展开/收起')
Column() {
Text('我是一张卡片')
.fontSize(18)
.margin({ bottom: 8 })
if (this.expanded) {
Text('展开后的内容:这里可以放描述、按钮、表单…')
.opacity(0.85)
}
}
.padding(12)
.borderRadius(16)
.height(this.expanded ? 180 : 88) // ⚠️高度变化会影响布局,注意范围别太大
.backgroundColor(Color.White)
.animation({ duration: 260, curve: Curve.EaseInOut })
Button(this.expanded ? '收起' : '展开')
.onClick(() => {
// 思路:把状态变化集中发生(别在多处乱改)
this.expanded = !this.expanded
})
}
.padding(16)
.backgroundColor(0xF6F7F9)
}
}
2.3 Transition:组件进出场别“闪现”,要有仪式感✨
最经典的:加载态 → 内容态;错误态 → 重试态;弹层进出。
@Entry
@Component
struct TransitionDemo {
@State showPanel: boolean = false
build() {
Column({ space: 16 }) {
Button(this.showPanel ? '隐藏面板' : '显示面板')
.onClick(() => this.showPanel = !this.showPanel)
if (this.showPanel) {
Column() {
Text('我是一个进出场面板').fontSize(16)
Text('别让我闪现好吗…').opacity(0.7)
}
.padding(12)
.borderRadius(16)
// 进入/退出用 transition(示意:位移+透明度)
.transition({
type: TransitionType.All,
// 不同版本的字段名可能略不同,你按你工程的 Transition API 适配即可
})
.opacity(1)
}
}
.padding(16)
}
}
小技巧:转场尽量用 opacity/translate/scale 这类“合成友好”的属性,少动布局属性。
3)性能优化技巧:动画要顺,先学会“别作死”😅
3.1 优先动画“合成属性”,少动画“布局属性”
- ✅ 推荐:
opacity、scale、translate、rotate - ⚠️ 谨慎:
height/width/padding/margin(会触发布局测量与重排)
我的经验:
能用“缩放+透明度”做出来的效果,就别用“高度从 0 拉到 300”硬撑。
那不是动画,那是对渲染管线的精神攻击😤
3.2 限制动画影响范围:别一动就让整棵树重组
- 把动画尽量放在小组件上
- 大页面拆分成多个子组件,减少重组面积
- 列表里避免每一项都挂复杂动画(尤其是滚动时)
3.3 动画时别做重活:build 里别算大数据/别发网络
动画期间一旦主线程忙,帧就掉。掉帧一多,用户主观感受是:这个 App 不行(哪怕你功能写得再牛也白搭🙂)
3.4 状态更新要“节制”:高频状态变化=高频重组=帧率暴毙
- 输入框联动动画别每次字符变化都触发全局动画
- 拖拽跟手动画尽量用更轻的方式(必要时做节流)
4)复杂动画实战:做一个“点赞爆炸”按钮(缩放+旋转+粒子假象)🔥
来点能拿去装进项目的东西:一个点赞按钮,点击后:
- 按钮轻微缩放回弹
- 心形旋转一点点
- 周围冒出几颗“伪粒子”(用小圆点/小图标模拟,够用又省性能)
4.1 代码:LikeButton(可复用组件)
@Component
struct LikeButton {
@State liked: boolean = false
@State burst: boolean = false
private triggerBurst() {
this.burst = true
// 粒子持续时间:和动画时长一致
setTimeout(() => this.burst = false, 380)
}
build() {
Stack() {
// “伪粒子层”
if (this.burst) {
ForEach([0, 1, 2, 3, 4, 5], (i: number) => {
// 6 个点往不同方向飞:用 rotate + translate 模拟
Circle()
.width(6)
.height(6)
.fill(Color.Red)
.opacity(0.9)
.translate({
x: 0,
y: -22
})
.rotate({ angle: i * 60 })
.animation({ duration: 380, curve: Curve.EaseOut })
})
}
// 主按钮
Text(this.liked ? '❤️' : '🤍')
.fontSize(34)
.rotate({ angle: this.liked ? -8 : 0 })
.scale({ x: this.liked ? 1.12 : 1, y: this.liked ? 1.12 : 1 })
.animation({
duration: 220,
curve: this.liked ? Curve.Spring : Curve.EaseOut
})
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor(0xFFFFFF)
.onClick(() => {
// 状态切换 + 触发 burst
this.liked = !this.liked
if (this.liked) this.triggerBurst()
})
}
}
4.2 使用方式:直接塞进页面
@Entry
@Component
struct LikeDemoPage {
build() {
Column({ space: 16 }) {
Text('复杂动画实战:点赞爆炸(但别真把帧率炸了🤣)')
.fontSize(16)
LikeButton()
Text('写动画的底线:好看、可控、不卡。')
.opacity(0.7)
}
.padding(16)
.backgroundColor(0xF6F7F9)
}
}
这套为什么“看起来复杂但不太吃性能”?
- 主要用
scale/rotate/translate/opacity(合成属性友好) - 粒子数量固定少(6 个)
- 生命周期短(380ms 自动消失)
- 没有在动画里触发布局重排的大范围变化
收尾:动画写到最后,拼的不是花活,是“克制”😌
你如果只记住一件事:
把动画当成“用户体验的语气词”,而不是“性能的敌人”。
轻一点、准一点、少一点,反而更高级✨
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:某个被流“治愈”过的 移动端 老兵
📅 日期:2025-11-05
🧵 本文原创,转载请注明出处。
更多推荐




所有评论(0)