你真想把鸿蒙动画“卷”到手感上头、交互顺到停不下来吗?

🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8
前言
动画不是“锦上添花”,是信息表达、结构引导和心流维持的低延迟语言。本文用 **ArkUI(Stage 模型 / ArkTS)**把四件事讲透:动画 API 与设计取舍、关键帧思路、手势系统、以及自定义组件的实战套路。不端术语汤,直接能跑的代码 + 设计心法 + 性能要点,一次打包。
一、动画的三层用法:从“省心”到“可控”
先搭好心智模型:隐式 → 过渡 → 显式,用最省事的能满足需求就别上高配。
- 隐式动画(Implicit):给组件挂一个
.animation({...}),当可动画属性(如opacity/scale/translate/rotate等)变化时自动过渡,最省心。 - 过渡(Transition):用
.transition(TransitionEffect.xxx())指定出现/消失的动效(入场、退场),对页面/列表尤实用。 - 显式动画(animateTo):用
animateTo({ duration, curve, delay }, () => { /* 改状态 */ })手动控制,适合串联、时序、条件分支等复杂场景。
二、动画 API 快速手册(够用又清楚)
2.1 隐式动画:一行“挂件”
// 隐式动画:状态一变,动画就走
@Entry
@Component
struct ImplicitDemo {
@State show: boolean = false
build() {
Column({ space: 16 }) {
Button(this.show ? 'Hide' : 'Show')
.onClick(() => this.show = !this.show)
// 可动画属性:opacity/scale/rotate/translate/size 等
Image($r('app.media.poster'))
.width(220).height(140)
.opacity(this.show ? 1 : 0)
.scale(this.show ? { x: 1, y: 1 } : { x: 0.92, y: 0.92 })
.animation({
duration: 220,
curve: Curve.EaseInOut, // 选个中性的缓动
})
.borderRadius(16)
}.padding(24)
}
}
使用心法
- 用隐式动画做局部过渡/微反馈(按钮、卡片 hover、like 点赞)。
- 一个组件挂一个
.animation()就够;避免里外多层叠加导致时间/曲线“打架”。
2.2 过渡动画:出场与退场的“门面担当”
// Transition:对列表插入/删除、页面入场/退场很友好
@Entry
@Component
struct TransitionDemo {
@State items: number[] = [1,2,3]
build() {
Column({ space: 12 }) {
Row({ space: 8 }) {
Button('Add').onClick(() => this.items.unshift(Date.now()))
Button('Pop').onClick(() => this.items.shift())
}
List() {
ForEach(this.items, (it) => {
ListItem() {
Text(`Item ${it}`).fontSize(18).padding(12)
.backgroundColor('#181818').borderRadius(10)
.transition(TransitionEffect
.asymmetric( // 入场 / 退场分开设
TransitionEffect.move(TransitionEdge.Start).combined(TransitionEffect.opacity(0.0)),
TransitionEffect.scale({ x: 0.9, y: 0.9 }).combined(TransitionEffect.opacity(0.0))
)
)
}.margin({ bottom: 8 })
}, (it) => it) // key
}
.layoutWeight(1)
}.padding(20)
}
}
使用心法
- 入退场分设:
asymmetric(appear, disappear)让进入更有“引导感”,离开更“干净”。 - 对长列表,退场动画别太慢(≤200ms),否则会阻塞布局回收。
2.3 显式动画:可编排、可插值、可打点
// 显式动画:完整掌控时间线与状态切换
@Entry
@Component
struct AnimateToDemo {
@State x: number = 0
@State scaleVal: number = 1
private async bounce() {
// 串联式时序
animateTo({ duration: 180, curve: Curve.EaseOut }, () => this.x = 36)
await sleep(180)
animateTo({ duration: 160, curve: Curve.EaseInOut }, () => this.x = 0)
// 叠加一个缩放
animateTo({ duration: 120 }, () => this.scaleVal = 1.06)
await sleep(120)
animateTo({ duration: 120 }, () => this.scaleVal = 1.0)
}
build() {
Column({ space: 16 }) {
Button('Bounce').onClick(() => this.bounce())
Text('Tap me')
.fontSize(20).padding(16)
.translate({ x: this.x })
.scale({ x: this.scaleVal, y: this.scaleVal })
.backgroundColor('#202020').borderRadius(12)
}.padding(24)
}
}
function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) }
使用心法
- 可插入逻辑:显式动画里可以做条件判断、埋点、并行动画。
- 统一曲线:多段动画尽量用相近的
curve,否则会有“抖音式断感”。
三、关键帧设计:不是死磕 API,是运动剧本
ArkUI 没有“必用 Keyframe 类才叫关键帧”的刚性要求。关键帧是设计方法:把运动拆成若干“转折点”,再用
animateTo串起来;或把多个隐式动画参数“对齐到时间点”。
3.1 关键帧脚本化(可维护)
type KF = { t: number; state: () => void } // t: 相对时间(ms)
function runKeyframes(frames: KF[]) {
let t = 0
frames.forEach(f => t = t + f.t)
// 顺序执行
let elapsed = 0
frames.reduce((p, f) => {
return p.then(() => {
return new Promise<void>(resolve => {
animateTo({ duration: f.t, curve: Curve.EaseInOut }, () => f.state())
setTimeout(resolve, f.t)
})
})
}, Promise.resolve())
}
// 使用
@Entry
@Component
struct KeyframeDemo {
@State opacity: number = 0.0
@State y: number = 24
@State blur: number = 12
play() {
runKeyframes([
{ t: 120, state: () => { this.opacity = 1; this.y = 0 } }, // 0%→40%
{ t: 80, state: () => { this.blur = 6 } }, // 40%→67%
{ t: 60, state: () => { this.blur = 0 } }, // 67%→100%
])
}
build() {
Column({ space: 12 }) {
Button('Play Keyframes').onClick(() => this.play())
Text('Keyframe Card')
.opacity(this.opacity)
.translate({ y: this.y })
.blur(this.blur)
.padding(18).backgroundColor('#1d1d1d').borderRadius(12)
}.padding(24)
}
}
设计心法
- 先写剧本,再码参数:先画 0%/40%/60%/100% 的姿态草图(位移、缩放、透明),再落到数值。
- 关键帧越少越纯粹:推荐 2–4 个转折点;多了容易“累赘”。
- 节律一致:同一页面里的不同元素对齐时间点(例如共同在 120ms 时到位),层级自然。
四、手势系统:把“手上意图”翻译成 UI 的语言
4.1 常用手势与事件
TapGesture():点击/双击。LongPressGesture({ duration }):长按。PanGesture({ direction, distance }):拖拽,可取速度。PinchGesture()/RotationGesture():捏合/旋转。GestureGroup(Parallel/Sequence):并行/串行组合(比如按住再拖)。- 事件回调:
.onActionStart(e) / .onActionUpdate(e) / .onActionEnd(e)。
4.2 手势 + 动画 = “流畅且可控”的本体
@Entry
@Component
struct PanCard {
@State offsetX: number = 0
@State offsetY: number = 0
@State scaleVal: number = 1
private settle() {
// 回弹:显式动画做“自然落座”
animateTo({ duration: 180, curve: Curve.EaseOut }, () => {
this.offsetX = 0; this.offsetY = 0; this.scaleVal = 1
})
}
build() {
let pan = PanGesture({ direction: PanDirection.All })
.onActionStart(() => {
animateTo({ duration: 100 }, () => this.scaleVal = 1.02) // 轻微提起
})
.onActionUpdate((e) => {
this.offsetX += e.offsetX
this.offsetY += e.offsetY
})
.onActionEnd((e) => {
// 速度阈值:快甩出去就“解散”,否则回弹
const v = Math.hypot(e.velocityX ?? 0, e.velocityY ?? 0)
if (v > 1400 || Math.abs(this.offsetX) > 140) {
animateTo({ duration: 220, curve: Curve.EaseOut }, () => {
this.offsetX += Math.sign(this.offsetX || 1) * 240
this.scaleVal = 0.86
})
// 结束:模拟消失
setTimeout(() => { this.offsetX = 0; this.offsetY = 0; this.scaleVal = 1 }, 260)
} else {
this.settle()
}
})
Column() {
Text('Drag me')
.translate({ x: this.offsetX, y: this.offsetY })
.scale({ x: this.scaleVal, y: this.scaleVal })
.padding(24)
.borderRadius(16)
.backgroundColor('#222')
.gesture(pan) // 将手势“挂”到组件
}.padding(24)
}
}
手势心法
- 开始就给“起势”(放大 1–2%/提高海拔感),结束要“落座”(回弹或拆分两段动画)。
- 速度优先于位移:甩出删除、回弹收起,用速度阈值更贴近人手感。
五、自定义组件动画实践:把通用动效封装成“乐高积木”
5.1 例 1:可复用的“弹簧卡片”🧩
目标:任意子内容都能套上“弹簧式按压 + 回弹”手感(按下缩小、释放反弹)。
// SpringCard.ets —— 简化弹簧手感(用显式动画近似)
@Component
export struct SpringCard {
@State private scaleVal: number = 1
@Prop contentBuilder: () => any // 透传子内容
build() {
GestureListener()
.onDown(() => animateTo({ duration: 80, curve: Curve.EaseIn }, () => this.scaleVal = 0.96))
.onUp(() => animateTo({ duration: 160, curve: Curve.EaseOut }, () => this.scaleVal = 1.0))
Column() {
this.contentBuilder()
}
.scale({ x: this.scaleVal, y: this.scaleVal })
.borderRadius(16).backgroundColor('#1a1a1a')
.padding(12)
.gesture(
TapGesture().onAction(() => {
// 可选:点击事件透传/反馈
})
)
}
}
使用
@Entry
@Component
struct SpringUse {
build() {
SpringCard({ contentBuilder: () => Text('Buy Now').fontSize(18).padding(10) })
.margin(20)
SpringCard({ contentBuilder: () => Image($r('app.media.poster')).width(260).height(150) })
.margin(20)
}
}
封装心法
- 把交互与动画参数(时长、曲线、缩放幅度)暴露为
@Param,即可在不同场景“拧旋钮”。 - 统一“起/落”曲线,按压快、回弹慢更接近物理直觉。
5.2 例 2:关键帧 Banner(进场→强调→稳定)
目标:页面打开时 Banner 先出现,轻微强调后稳定,同时配合手势左右滑。
@Entry
@Component
struct BannerKF {
@State x: number = 18 // 初始右移
@State blur: number = 10
@State op: number = 0.0
@State scale: number = 0.98
aboutToAppear() { this.play() }
async play() {
// 0%→60%:入场
animateTo({ duration: 180, curve: Curve.EaseOut }, () => {
this.x = 0; this.op = 1; this.blur = 0
})
await sleep(180)
// 60%→80%:强调(轻微放大)
animateTo({ duration: 100, curve: Curve.EaseInOut }, () => this.scale = 1.02)
await sleep(100)
// 80%→100%:稳定回落
animateTo({ duration: 120, curve: Curve.EaseInOut }, () => this.scale = 1.0)
}
build() {
let pan = PanGesture({ direction: PanDirection.Horizontal })
.onActionUpdate(e => this.x += e.offsetX)
.onActionEnd(() => animateTo({ duration: 140 }, () => this.x = 0))
Column() {
Image($r('app.media.banner'))
.width('100%').height(160)
.translate({ x: this.x })
.scale({ x: this.scale, y: this.scale })
.opacity(this.op)
.blur(this.blur)
.borderRadius(16)
.gesture(pan)
}.padding(16)
}
}
六、设计与工程双清单(做完这 12 条再下班)
设计侧(动效稿)
- 定义目的:引导/反馈/层级/情绪?不同目的决定时长与曲线。
- 写关键帧脚本(0/60/100%)与延迟关系(谁先动、谁跟随)。
- 统一曲线家族:同一页面尽量只用 2–3 种 curve。
工程侧(ArkUI 落地)
4. 优先用隐式/过渡,复杂再上 animateTo。
5. 手势与动画合拍:onActionStart 起势、onActionEnd 落座。
6. 列表退场 ≤200ms;入场 ≤260ms,二者节律一致。
7. 封装可复用组件(SpringCard、BannerKF 等),参数化。
8. 动画与状态最小耦合:只让 @State 管控需要过渡的字段。
9. 避免多层 .animation() 叠加;用一个“总动画”覆盖。
10. 并行动画合并触发,减少多次 re-layout。
11. 弱设备降级:图片滤镜/模糊优先关;形变代替阴影。
12. 埋点:动效开始/结束记录,方便 A/B 与卡顿排查。
七、常见坑位与解法
- 首帧闪烁:
aboutToAppear()中先把初始状态设置到“起点”,再触发动画。 - 卡顿:把 IO/网络从生命周期挪走;动画只改数字型属性(位移/缩放/透明),尽量避开频繁重排。
- 手势与滚动冲突:横向
PanGesture避免与纵向List抢手,必要时用GestureGroup.Sequence或在阈值前不拦截。 - 曲线乱用:一个页面混入多种夸张弹性曲线,主观“花却不顺”。同一族的
EaseIn/EaseOut/EaseInOut足矣。
八、可复制的“动画样式表”(拿去即用)
export const Motion = {
fast: { duration: 120, curve: Curve.EaseOut },
base: { duration: 180, curve: Curve.EaseInOut },
slow: { duration: 260, curve: Curve.EaseInOut },
pressIn: { duration: 80, curve: Curve.EaseIn },
pressOut: { duration: 160, curve: Curve.EaseOut },
}
export const Transitions = {
listAppear: TransitionEffect.opacity(0.0)
.combined(TransitionEffect.move(TransitionEdge.Start)),
listDisappear: TransitionEffect.opacity(0.0)
.combined(TransitionEffect.scale({ x: 0.92, y: 0.92 }))
}
把它当“动效 Design Token”。项目统一调用,体验可控、维护省心。
结语:动画不是“秀”,是信息结构与情绪的放大器
当你用隐式抹平小摩擦、用过渡梳理结构、用显式表达节奏,再把手势接进来让用户“像拉一根橡皮筋”那样操作——你的界面就会有生命力。
现在的问题是:你打算先把哪个页面“动”起来?是首页 Banner 的“进→强调→稳”,还是商品卡片的“按→弹→落”?😉
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」专栏(全网一个名),bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。
✨️ Who am I?
我是bug菌(全网一个名),CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主/价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-
- 点赞
- 收藏
- 关注作者
评论(0)