为什么你的 ArkUI 动画总差点“灵魂”?显式/隐式、控制器、曲线我都给你掰开揉碎讲!
这篇文章系统讲解了HarmonyOS ArkUI动画开发的核心技术要点。作者从显式动画和隐式动画的区别入手,通过代码示例展示了两种动画的实现方式:隐式动画适合简单状态变化场景,通过.animation()修饰器或animateTo()方法实现;显式动画则更适合需要精确控制的复杂动画。文章重点介绍了AnimationController的使用技巧,包括动画生命周期控制、进度跳转、反向播放等高级功能,
我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~
前言
先问你个扎心的问题:你做的 ArkUI 动画,是“动了”,还是“动人”?很多同学第一次上手动画,能把元素从 A 移到 B、能把透明度从 0 到 1,就觉得万事大吉。可一上真场景——卡顿、延迟、节拍不对、手势不跟手、交互动线不统一……用户看着就是“别扭”。别急,本文就带你系统拆解 ArkUI 动画系统:从显式/隐式动画的本体哲学,到AnimationController的“指挥棒”,再到Curve(曲线)与插值器如何“点睛”。全程都用可以直接拿去跑的 ArkTS 代码,配上我踩坑踩出来的心得体感,保证你读完能把动画做得“又丝滑又聪明”。😎
适读对象:已经在写 ArkUI(ArkTS)的同学,想把动画从“能跑”升级到“好用、好看、好维护”。
导航(按大纲来)
- 显式动画 / 隐式动画:两种建模方式怎么选,什么时候各显神通?
- 动画控制器(AnimationController):如何暂停、恢复、反向、跳转进度、和手势绑定?
- 曲线与插值器(Curve):为什么“快—慢—停”才有生命力?自定义贝塞尔、弹簧曲线怎么玩?
一、显式动画 vs 隐式动画:一静一动,动静皆宜
1.1 隐式动画:写状态就给你“顺手带个动效”
先用一句人话总结隐式动画:你只管改状态,ArkUI 帮你把变更用动画“抹平”。
在 ArkUI 中,隐式动画通常有两种常见写法:
- 组件级修饰器
.animation({ ... }):给某个组件绑定一条“默认动画规则”;只要这个组件的可动画属性(如opacity、rotate、scale、translate、width/height等)发生变化,就按规则“动”。 animateTo(options, () => { ... }):把一批状态变更包起来,一次性“平滑过渡”。
什么时候用隐式?交互简单、只需要“随状态一起动”的场景:比如按钮按下的缩放、卡片展开、列表项淡入淡出、细微的位移修饰等。
示例:给卡片一个“呼吸感”
@Component
export struct BreathingCardDemo {
@State private on = false;
build() {
Column() {
// 卡片本体
Column() {
Text(this.on ? 'I am alive ✨' : 'Tap me!')
.fontSize(18)
.fontColor('#ffffff')
.padding(16)
}
.width(220)
.height(120)
.borderRadius(16)
.backgroundColor(this.on ? '#4B7BEC' : '#9B9B9B')
// 注意:给组件绑一个隐式动画规则
.animation({
duration: 320,
curve: Curve.EaseInOut
})
// 状态变化时,以下属性会被上述规则“抹平过渡”
.scale(this.on ? 1.08 : 1.0)
.opacity(this.on ? 1.0 : 0.85)
.onClick(() => this.on = !this.on)
// 控制按钮
Button(this.on ? '收一收' : '展开呼吸')
.margin({ top: 20 })
.onClick(() => this.on = !this.on)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor('#121212')
}
}
心得:单点轻动效走隐式最省脑,代码“语义洁净”,不会满屏都是控制器对象。
1.2 显式动画:你来掌舵,我只负责推帆
显式动画强调“我明确地告诉你什么时候开始、做什么、怎么做”。典型接口是 animateTo(),也可以配合动画控制器实现更细粒度控制(第二章展开)。
示例:一键展开/收起,多个状态联动
@Component
export struct ExpandPanelDemo {
@State private expanded = false;
@State private contentOpacity: number = 0.0;
@State private contentHeight: number = 0;
private targetHeight = 140;
toggle() {
// 使用 animateTo 将一批状态变更合在一段动画里
animateTo({
duration: 360,
curve: Curve.EaseInOut,
onFinish: () => console.info('[ExpandPanelDemo] transition done')
}, () => {
this.expanded = !this.expanded
this.contentOpacity = this.expanded ? 1.0 : 0.0
this.contentHeight = this.expanded ? this.targetHeight : 0
})
}
build() {
Column() {
Row() {
Text('订单详情')
.fontSize(20).fontWeight(FontWeight.Bold)
Blank()
Button(this.expanded ? '收起' : '展开')
.onClick(() => this.toggle())
}
.height(48).padding({ left: 16, right: 16 }).alignItems(VerticalAlign.Center)
// 内容区:高度与透明度在 animateTo 中联动
Column() {
Text('· 商品名称:ArkUI Phone')
Text('· 价格:¥3999')
Text('· 配送:当日达(首发专享)')
}
.height(this.contentHeight)
.opacity(this.contentOpacity)
.padding(16)
.clip(true) // 重要:收起时不露馅
.backgroundColor('#202020')
.borderRadius(12)
.margin({ top: 8 })
.animation({ duration: 360, curve: Curve.EaseInOut }) // 也可不写,靠 animateTo 就行;写了会更“顺”
}
.padding(16)
.backgroundColor('#121212')
.width('100%').height('100%')
}
}
心得:多属性联动、需要 onFinish 回调、需要一次性控制多个组件时,
animateTo非常趁手。它像个“事务”(transaction):把变化集中交给动画系统,一起“平滑提交”。
二、AnimationController:让动画听你的指挥棒
隐式/显式能解决大半需求,但当你要做可暂停、可反向、可拖拽、可预览,甚至多段串联的复杂动画,控制器(AnimationController)就登场了。简单说,它是把动画时间轴抽象成一个对象,你可以:
- play / pause / resume / stop / finish:控制生命周期
- reverse:反向播放(常见于返回动效、切换动线)
- setProgress / progress:把手势位移 → 动画进度,做“跟手”
- onFrame / onFinish:订阅帧事件,用于同步其他状态或触发后续逻辑
不同版本模板 API 略有差异,下面示例采用通用写法语义;若你的工程模板方法名略有不同(如
forward()/backward()),替换即可,思路完全一致。
2.1 基础用法:从 0 到 1 的时间轴
@Component
export struct ControllerBasics {
private controller: AnimationController = new AnimationController({
duration: 500,
curve: Curve.EaseInOut
})
@State private x = 0
@State private opacity = 1.0
aboutToAppear() {
// 每一帧回调(可选):把进度同步成你想要的数值
this.controller.onFrame((progress: number) => {
// progress 通常是 0~1
this.x = 40 * progress
this.opacity = 1.0 - 0.3 * progress
})
this.controller.onFinish(() => console.info('[ControllerBasics] finished'))
}
build() {
Column() {
Row() {
Button('Play').onClick(() => this.controller.play())
Button('Pause').margin({ left: 8 }).onClick(() => this.controller.pause())
Button('Resume').margin({ left: 8 }).onClick(() => this.controller.resume())
Button('Reverse').margin({ left: 8 }).onClick(() => this.controller.reverse())
}
.margin(16)
// 被控制的“盒子”
Row() {
Text('🐦')
.fontSize(22)
}
.width(100).height(56)
.backgroundColor('#2D7D46')
.borderRadius(16)
.translate({ x: this.x, y: 0 })
.opacity(this.opacity)
}
.width('100%').height('100%')
.backgroundColor('#121212')
}
}
要点:把“UI属性 = f(progress)” 这种关系写清楚,你的动画就变成了“可编排的时间函数”。控制器只是负责推进 progress。
2.2 跟手动画:滑多少,动多少(手势 ←→ 进度)
真正让动效“活起来”的,是它和手势的配合。比如一个“底部抽屉”需要跟着手指拖动,并在松手时按速度/距离决定“展开还是收起”。
@Component
export struct BottomSheetDemo {
private controller: AnimationController = new AnimationController({ duration: 360, curve: Curve.EaseOut })
@State private progress: number = 0 // 0=收起, 1=完全展开
private minY = 0
private maxY = 320 // sheet 最大拖动高度
aboutToAppear() {
this.controller.onFrame((p: number) => this.progress = p)
}
private toY(p: number) { return this.maxY * (1 - p) } // p=1 → y=0(完全展开)
build() {
Stack() {
// 模拟内容区
Column() { Text('Content').fontSize(20).fontColor('#fff') }
.width('100%').height('100%').backgroundColor('#1E1E1E')
// 底部抽屉
Column() {
Row() { Text('Drag me ↑').fontSize(16) }.height(56).alignItems(VerticalAlign.Center)
// ... 放更多内容
}
.width('100%')
.height(this.maxY + 80)
.backgroundColor('#2B2B2B')
.borderRadius({ topLeft: 16, topRight: 16 })
.translate({ x: 0, y: this.toY(this.progress) })
.gesture(
PanGesture({ direction: PanDirection.Vertical, distance: 1 })
.onActionStart(() => this.controller.pause())
.onActionUpdate((event) => {
// 根据手指位移更新进度
const dy = -event.offsetY // 向上为正
const travelled = (this.progress * this.maxY) + dy
const next = Math.min(Math.max(travelled / this.maxY, 0), 1)
this.progress = next
this.controller.setProgress(next) // 关键:用控制器写回进度
})
.onActionEnd((event) => {
// 松手后根据速度/位置决定去向
const goingUp = event.velocityY < 0
const shouldOpen = goingUp ? true : (this.progress > 0.5)
if (shouldOpen) this.controller.play() // 朝1播放
else this.controller.reverse() // 朝0播放
})
)
}
.width('100%').height('100%')
}
}
关键点:
- 进度是“一等公民”,UI 属性基于进度派生。
- 手势期间
pause()冻住时间轴,用setProgress()直接推进;松手后play()/reverse()让动画自己“收尾”。- 这套套路也非常适合轮播图、卡片叠层、转场过度等场景。
2.3 串联与并联:一段接一段、一起走一起停
实际效果往往需要多段动画组装:比如“淡入 + 位移 + 旋转”先后执行,或“头像和昵称同时进场”。
并联(together):多个属性同时基于同一 controller
@Component
export struct TogetherDemo {
private c = new AnimationController({ duration: 460, curve: Curve.FastOutSlowIn })
@State private p = 0
aboutToAppear() { this.c.onFrame((x) => this.p = x) }
build() {
Column() {
Button('Play').onClick(() => this.c.play())
Row() {
// 同一个 progress,多个属性并联
Image($r('app.media.avatar'))
.width(80).height(80).borderRadius(40)
.opacity(this.p)
.scale(0.8 + 0.2 * this.p)
.rotate({ angle: 12 * (1 - this.p) })
.translate({ x: -40 * (1 - this.p), y: 0 })
Text('Hello ArkUI')
.fontSize(18).fontColor('#fff')
.opacity(this.p)
.translate({ x: 12 * (1 - this.p), y: 0 })
}
.margin(20)
}
.backgroundColor('#121212')
.width('100%').height('100%')
}
}
串联(sequence):分段推进
通常做法是:第一段 onFinish 里接第二段,或者使用多个控制器按顺序 play()。也可以自己封装一个**“时间切片”**的 Sequence,把全球时间进度映射到局部 0~1,再各自驱动。
// 一个简易 Sequence 工具,把全局 progress 分片给多段
function sliceProgress(global: number, start: number, end: number) {
if (global <= start) return 0
if (global >= end) return 1
return (global - start) / (end - start)
}
@Component
export struct SequenceDemo {
private c = new AnimationController({ duration: 800, curve: Curve.EaseInOut })
@State private p = 0
aboutToAppear() { this.c.onFrame(x => this.p = x) }
build() {
Column() {
Button('Play').onClick(() => this.c.play())
// 段1:0~0.4 透明度
const p1 = sliceProgress(this.p, 0.0, 0.4)
// 段2:0.4~0.7 位移
const p2 = sliceProgress(this.p, 0.4, 0.7)
// 段3:0.7~1.0 旋转
const p3 = sliceProgress(this.p, 0.7, 1.0)
Image($r('app.media.logo'))
.width(96).height(96).borderRadius(12)
.opacity(p1)
.translate({ x: 0, y: (1 - p2) * 24 })
.rotate({ angle: p3 * 180 })
}
.padding(24)
.backgroundColor('#121212')
.width('100%').height('100%')
}
}
亮点:串并联统一到“进度映射”这个抽象,你的动画就具备“可编排性”和“可维护性”。写复杂动效不再靠复制粘贴时间参数。
三、Curve(曲线)与插值器:让动画有“呼吸和肌肉”
“曲线”是动画气质的灵魂。同一个位移 40px,用 Linear 和用 EaseInOut,给人的感觉完全不是一个东西。ArkUI 的 Curve 提供常用枚举(如 Linear、Ease、EaseIn、EaseOut、EaseInOut、FastOutSlowIn 等),同时支持自定义贝塞尔。此外,很多同学喜欢的“弹簧感”,也可以通过参数化曲线/插值器来实现。
3.1 你真的了解常见曲线吗?
- Linear:匀速,机械。适合加载条、闪烁这类“不要情绪”的地方。
- EaseIn:先慢后快。适合入场(像“加速进入画面”)。
- EaseOut:先快后慢。适合离场(像“刹车停靠”)。
- EaseInOut:慢→快→慢。通用好用,大多数 UI 过渡都挺优雅。
- FastOutSlowIn:更“急促地出发、更柔和地停下”,有鲜明张力,适配卡片升起/按钮按下回弹等。
对比示例:同一段位移,不同曲线体验
@Component
export struct CurveCompare {
@State private xLinear = 0
@State private xEase = 0
@State private xFosi = 0 // FastOutSlowIn
play(curve: Curve, setter: (v:number) => void) {
const c = new AnimationController({ duration: 600, curve })
c.onFrame((p) => setter(p * 180))
c.play()
}
build() {
Column() {
Row() {
Button('Linear').onClick(() => this.play(Curve.Linear, v => this.xLinear = v))
Button('EaseInOut').margin({ left: 8 }).onClick(() => this.play(Curve.EaseInOut, v => this.xEase = v))
Button('FastOutSlowIn').margin({ left: 8 }).onClick(() => this.play(Curve.FastOutSlowIn, v => this.xFosi = v))
}.margin(16)
// 三条小条同时比较
Column() {
Row().width(200).height(2).backgroundColor('#333')
Row().width(12).height(12).borderRadius(6)
.backgroundColor('#5AD') .translate({ x: this.xLinear, y: -5 })
Row().width(200).height(2).backgroundColor('#333').margin({ top: 22 })
Row().width(12).height(12).borderRadius(6)
.backgroundColor('#7ED') .translate({ x: this.xEase, y: 17 })
Row().width(200).height(2).backgroundColor('#333').margin({ top: 22 })
Row().width(12).height(12).borderRadius(6)
.backgroundColor('#9F8') .translate({ x: this.xFosi, y: 39 })
}
}
.width('100%').height('100%').backgroundColor('#121212')
}
}
看着它们跑一遍,你就会对“曲线带来的气质变化”更敏感。
3.2 自定义贝塞尔:把美术同学的“动效规范”落地到代码
设计稿里常出现这样的描述:“cubic-bezier(0.2, 0.8, 0.2, 1)”。你完全可以在 ArkUI 里还原:
const EaseBrand = Curve.cubicBezier(0.2, 0.8, 0.2, 1.0)
@Component
export struct BrandMotionDemo {
private c = new AnimationController({ duration: 520, curve: EaseBrand })
@State private p = 0
aboutToAppear() { this.c.onFrame(x => this.p = x) }
build() {
Column() {
Button('Play').onClick(() => this.c.play())
Text('Brand Motion').fontSize(22).fontColor('#fff')
.opacity(this.p)
.translate({ x: 0, y: (1 - this.p) * 16 })
}.padding(24)
.backgroundColor('#121212')
.width('100%').height('100%')
}
}
小技巧:把品牌曲线抽象成常量,形成“动效规范”。产品动效统一、体验“像一家人”。
3.3 春天里最会“弹”的那位:弹簧/阻尼曲线(Spring)
很多时候你要的是物理感:比如按钮按下有个轻微回弹,卡片被拉出后有弹性的回归。这时可以用弹簧曲线(不同模板名称可能略有差异,通常在 Curve 下提供 spring 参数 / 或有诸如 Spring、SpringMotion 的接口),核心参数是刚度(stiffness)和阻尼(damping)。
下面用一种常见写法:自行实现一个简化版弹簧插值(实战可换成内置 Spring 接口/曲线)。
// 一个简单的弹簧插值器:给定 t∈[0,1],返回带弹性的过渡
function springLerp(t: number, damping = 10, stiffness = 180): number {
// 近似:阻尼振动解(可替换成项目内置的 Spring 曲线)
// 这里只为了展示效果,公式做轻量化处理
const w0 = Math.sqrt(stiffness)
const zeta = damping / (2 * Math.sqrt(stiffness))
if (zeta < 1) {
const wd = w0 * Math.sqrt(1 - zeta * zeta)
return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd * t) + (zeta / Math.sqrt(1 - zeta * zeta)) * Math.sin(wd * t))
} else {
// 过阻尼近似
return 1 - Math.exp(-w0 * t)
}
}
@Component
export struct SpringFeelDemo {
private c = new AnimationController({ duration: 700, curve: Curve.Linear })
@State private p = 0
aboutToAppear() { this.c.onFrame(x => this.p = x) }
build() {
const sp = springLerp(this.p, 12, 180) // 弹簧后的“进度”
Column() {
Button('Boing!').onClick(() => this.c.play())
Row()
.width(100).height(56).borderRadius(28)
.backgroundColor('#3E9')
.scale(0.8 + 0.3 * sp)
.shadow({ radius: 12 * sp, color: '#3E9' })
}
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.width('100%').height('100%').backgroundColor('#121212')
}
}
真实项目里优先用内置 Spring 曲线 API(如果你的模板版本提供),因为它对帧率、数值稳定性更友好。自己写则记得限制 overshoot,避免“弹过头挡住东西”。
四、把“动画工程化”:规范、解耦、复用、可调试
好看的动画不少见,好维护的动画才稀缺。下面给你一套“工程化小抄”。
4.1 抽象“可复用动效组件”
把常见动效抽象成组件或函数,让调用方只关心“想要的感觉”,而不是每次从头配时间、曲线。
例:通用“淡入上移”动效
type MotionPreset = {
duration?: number
curve?: Curve
distance?: number
}
function fadeUp(controller: AnimationController, p: number, opt?: MotionPreset) {
const d = opt?.distance ?? 12
const opacity = p
const y = (1 - p) * d
return { opacity, y }
}
@Component
export struct MotionPresetDemo {
private c = new AnimationController({ duration: 420, curve: Curve.EaseInOut })
@State private p = 0
aboutToAppear() { this.c.onFrame(x => this.p = x) }
build() {
const m = fadeUp(this.c, this.p, { distance: 18 })
Column() {
Button('Play').onClick(() => this.c.play())
Text('Preset Motion')
.fontSize(20).fontColor('#fff')
.opacity(m.opacity)
.translate({ x: 0, y: m.y })
}.padding(24).backgroundColor('#121212').width('100%').height('100%')
}
}
收益:形成团队动效词汇表。当你说“这个地方给我
fadeUp,300ms,EaseInOut”,大家都懂,不用来回调参。
4.2 统一时间与曲线:别让页面变“菜市场”
- 规范化:把常用
duration定成变量,比如T.Fast=160,T.Mid=320,T.Slow=560;曲线也命名,如Ease.Brand,Ease.Overlay。 - 分层:交互类(按钮反馈)用快节奏,布局类(页面进出)用中等,沉浸式(全屏过渡)允许稍慢。
- 一致性:同类场景统一曲线与时长,用户的肌肉记忆会点赞。
4.3 调试与性能:别用动效“装饰卡顿”
- 避免过度动画:列表长项每个都开 300ms 淡入?慎重。首屏要紧,优先关键节点。
- 合成层友好:尽量用不触发布局回流的属性(
opacity、transform族),减少width/height硬变更。 - 分段加载:复杂页面先完成骨架,再让“装饰型动效”慢半拍上场,感知更流畅。
- 低端机兜底:给动效挂“功耗档位”(例如全局开关或时长缩短),在性能吃紧的设备走轻量版。
五、真实案例:从按钮按压到卡片分层出场
5.1 按钮按压反馈(隐式动画 + 弹簧味)
@Component
export struct PressButton {
@State private down = false
build() {
Row()
.width(160).height(46)
.borderRadius(23)
.backgroundColor('#3A85FF')
.scale(this.down ? 0.95 : 1)
.shadow({ radius: this.down ? 6 : 12, color: '#3A85FF' })
.animation({ duration: 120, curve: Curve.FastOutSlowIn })
.onTouch((e) => {
if (e.type === TouchType.Down) this.down = true
if (e.type === TouchType.Up || e.type === TouchType.Cancel) this.down = false
})
.justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center)
.onClick(() => console.info('clicked'))
.bindContent(
Text('加入购物车').fontSize(16).fontColor('#fff')
)
}
}
小而美:短时长 + 快出慢收,就是舒服。
5.2 卡片分层进场(Sequence:标题→图片→按钮)
@Component
export struct CardStaggerIn {
private c = new AnimationController({ duration: 900, curve: Curve.EaseInOut })
@State private p = 0
aboutToAppear() { this.c.onFrame(x => this.p = x) }
build() {
const titleP = sliceProgress(this.p, 0.00, 0.35)
const imageP = sliceProgress(this.p, 0.25, 0.70)
const actionP = sliceProgress(this.p, 0.60, 1.00)
Column() {
Button('进入').onClick(() => this.c.play())
Column() {
Text('ArkUI 动画系统')
.fontSize(22).fontWeight(FontWeight.Bold).fontColor('#fff')
.opacity(titleP)
.translate({ x: 0, y: (1 - titleP) * 10 })
Image($r('app.media.banner'))
.width('100%').height(160).borderRadius(12).margin({ top: 12 })
.opacity(imageP)
.scale(0.9 + 0.1 * imageP)
Row() {
Button('开始体验').onClick(() => console.info('start'))
.opacity(actionP).translate({ x: 0, y: (1 - actionP) * 12 })
}.margin({ top: 18 })
}
.padding(16)
.backgroundColor('#232323').borderRadius(16).width('100%')
}
.padding(16).backgroundColor('#121212').width('100%').height('100%')
}
}
一眼就“叙事化”了:标题告诉你我是谁,图片告诉你我做什么,按钮告诉你来不来。
六、FAQ:你可能卡的那些点
Q:animateTo 和 .animation() 同时写,会不会“打架”?
A:不会“打架”,但要理解二者关系。.animation() 是组件级默认,而 animateTo 是包裹一批状态改变。在实际项目中,要么统一走 animateTo 做一次性过渡,要么靠 .animation() 处理碎片化微动效。同一属性反复切换来源时,注意时长和曲线保持一致。
Q:控制器的 onFrame 会不会太频繁?
A:它就是逐帧,别滥用。推荐只在需要“多属性同步”的地方用;简单场景交给隐式/显式动画即可。记得解绑或关闭不需要的监听,避免长生命周期里“忘关水龙头”。
Q:手势动画为什么偶尔“跟不齐手”?
A:通常是把布局类属性也挂上了跟手(比如频繁改 height),导致重排。优先用 translate/scale/opacity,并在关键时刻 clip 裁切,让视觉正确但布局不抖。
Q:为什么我用 Linear 看起来“假”?
A:人的感知对加速度更敏感。线性匀速没有“力学”味道,显得机械。EaseOut 用在结束落座、EaseIn 用在起步,EaseInOut 通用。有条件就用品牌贝塞尔或弹簧做润色。
七、Checklist:上线前动效质检表
- 统一时间/曲线:是否使用动效规范里的预设?
- 关键路径轻量:首屏、切页是否避免堆叠过多动画?
- 低性能兜底:是否支持全局开关/降级?
- 无障碍考虑:是否尊重“减少动态效果(Reduce Motion)”的系统偏好?
- 与手势一致:拖拽类动效是否跟手、松手回弹是否合理?
- 不破布局:优先 transform/opacity,必要时 clip,避免频繁回流。
- 可测试:关键动线是否有演示页或 story,方便回归?
八、总结:动画不是“装饰”,它是“语言”
把动画理解成“装饰”,就容易走向“哪里都想动一下”的歧途;把动画当作语言,你会思考节奏、重音、语法:
- 进入时要铺陈(EaseIn),
- 停靠时要落座(EaseOut),
- 重要信息要分层叙事(Sequence/Stagger),
- 与手指要同频共振(Controller + setProgress),
- 品牌要统一口音(预设时长 + 曲线规范)。
当这些成为你的下意识选择,你做出来的界面,就不是简单的“有动画”,而是“有呼吸、有情绪、有态度”。
现在就挑一个页面:把开场的卡片进场改成 Sequence,把按钮反馈改成“快出慢收”,把抽屉改成“手势跟手+松手收尾”。你会惊讶:同一套 UI,气质立马不一样。
九、附录:常用 API 速查卡(按本文语义)
不同工程模板的具体签名可能略有差异,以下为通用语义与常用写法,迁移时替换为你项目的实际导入路径/枚举即可。
-
隐式动画(组件修饰)
.animation({ duration, curve, delay?, iterations?, playMode?, onFinish? })
对当前组件的可动画属性(如translate/scale/rotate/opacity)的变化生效。 -
显式动画(事务式)
animateTo(options, () => { /* 这里批量改状态 */ })options:duration, curve, delay?, onFinish?等。 -
动画控制器
new AnimationController({ duration, curve })
方法:play() / pause() / resume() / reverse() / finish() / setProgress(v:0~1)
事件:onFrame(cb: (progress)=>void),onFinish(cb)。 -
曲线(Curve)
Curve.Linear / Ease / EaseIn / EaseOut / EaseInOut / FastOutSlowIn / ...
自定义贝塞尔:Curve.cubicBezier(x1, y1, x2, y2)。
(如果你的模板支持)弹簧:Curve.Spring(...)或SpringMotion(...)。 -
手势配合
PanGesture({...}).onActionStart/Update/End
常见逻辑:pause()→setProgress()(跟手)→play()/reverse()(收尾)。
…
(未完待续)
更多推荐




所有评论(0)