界面太“死”还怪用户不爱点?ArkUI 动画这点活儿你得会!
本文介绍了ArkUI动画系统的核心功能与使用技巧。主要内容包括:1)三大动画类型(属性动画、关键帧动画和转场动画)的应用场景与选择建议;2)属性动画的两种实现方式(animateTo显式动画和.animation修饰器)的对比与适用场景;3)关键帧动画keyframeAnimateTo的分段控制方法,并通过弹跳徽标案例展示复杂动画的实现。文章强调ArkUI动画系统设计友好,能有效提升UI交互体验,
大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
前言
老实说,刚上手 ArkUI 的那会儿,我对动画的态度是:“能不用就不用,反正功能能跑就行。”
结果产品一句话把我教育了:
“页面是能用,但你这交互也太直男了吧,一点动效都没有,谁想点?”
后来我认真把 ArkUI 动画系统撸了一遍,才发现它其实不复杂,甚至可以说是很照顾开发者心情:
- 有属性动画负责各种属性渐变;
- 有显式动画 / 动画控制器负责复杂编排;
- 有关键帧动画负责“花里胡哨”的高级动效;
- 有转场动画帮你搞定组件进出、页面切换;
- 还支持跟手势做联动动画,比你自己算每一帧舒服多了。
今天这篇,就按你给的几个点,把 ArkUI Animation 从整体到实战撸一个“完全指南”,重点落在:
- 常见动画类型怎么选
AnimatedProperty(可动画属性)到底是什么鬼- 动画控制器怎么玩
- 手势联动怎么写得既跟手又不卡
- 以及几个实用 UI 动效案例
一、动画类型:属性、关键帧、转场,谁负责哪摊事?
ArkUI 里,动画大致可以拆成三类维度来看:
-
属性动画(Property Animation)
-
核心方式:
animateTo(...)显式动画(函数).animation({...})属性动画(修饰器)
-
用来让组件的各种“可动画属性”在一段时间内平滑变化,比如宽高、位置、透明度、圆角、缩放、旋转等等。
-
-
关键帧动画(Keyframe Animation)
- 接口:
UIContext.keyframeAnimateTo(param, keyframes) - 适合分段、多阶段变化,比如:放大 → 旋转 → 弹回 → 淡出这种复杂路径。
- 接口:
-
转场动画(Transition)
- 组件级转场:
.transition({...}) + animateTo - 页面级转场:导航 / router 的默认转场 & 自定义转场
- 用来控制“出现 / 消失 / 切换时怎么动”,比如淡入淡出、位移进出、缩放、共享元素等。
- 组件级转场:
一句粗暴总结:
- 想让“某个属性”动:用属性动画(
animateTo/.animation) - 想让“分段动,从 A → B → C”:用关键帧动画
- 想让“出现消失、列表增删、页面切换更顺滑”:用转场动画
二、属性动画:animateTo vs animation,怎么选?
属性动画基本是 ArkUI 动画的“地基”,先把这个玩明白,剩下的都是加特效。
2.1 显式动画 animateTo —— 闭包里的变化都做动画
核心形式:
this.getUIContext()?.animateTo(
{
duration: 300,
curve: Curve.EaseInOut
},
() => {
// 这里面所有引起 UI 变化的状态修改,都会按上面的参数做动画
}
);
比如一个按钮点击后移动 + 变宽:
@Entry
@Component
struct ExplicitAnimationDemo {
@State offsetX: number = 0;
@State btnWidth: number = 120;
build() {
Column() {
Button('点我动一下')
.width(this.btnWidth)
.translate({ x: this.offsetX })
.onClick(() => {
this.getUIContext()?.animateTo(
{ duration: 400, curve: Curve.EaseInOut },
() => {
this.offsetX = this.offsetX === 0 ? 80 : 0;
this.btnWidth = this.btnWidth === 120 ? 200 : 120;
}
);
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
适用场景:
- 需要对多个属性 / 多个组件统一用一组动画参数
- 动画逻辑比较集中,一次性触发
2.2 属性动画 .animation() —— 声明式绑定,谁变谁动
核心形式:
Image($rawfile('xxx.png'))
.opacity(this.opacity)
.scale({ x: this.scale, y: this.scale })
.animation({
duration: 500,
curve: Curve.Friction
})
只要绑定在 .animation() 之前的“可动画属性”发生变化,就自动按参数做动画。
举个常见的卡片动效:
@Entry
@Component
struct CardAnimationDemo {
@State cardSize: number = 200;
@State radius: number = 8;
@State angle: number = 0;
build() {
Column() {
Column() {
Text('Hello ArkUI')
}
.width(this.cardSize)
.height(this.cardSize)
.borderRadius(this.radius)
.rotate({ angle: this.angle })
.backgroundColor('#FFCCDD')
.animation({
duration: 600,
curve: Curve.EaseOut
})
.onClick(() => {
// 只管改状态,动画系统自动帮你补帧
this.cardSize = this.cardSize === 200 ? 260 : 200;
this.radius = this.radius === 8 ? 32 : 8;
this.angle = this.angle === 0 ? 5 : 0;
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
对比一下:
| 场景 | 推荐 |
|---|---|
| 一段逻辑里控制多处 UI 差异 | animateTo |
| 同一个组件不同属性用不同动画参数 | 多个 .animation() |
| 简单状态驱动动画(少改代码) | .animation() 优先 |
三、关键帧动画:keyframeAnimateTo 把动画拆成几段玩
当你想实现这种动效:
卡片先轻轻放大 → 稍微旋转一下 → 再弹回原位
普通动画也能勉强写,但会写一堆 setTimeout + animateTo,既丑又不好维护。
ArkUI 的关键帧动画就是为这种场景准备的:UIContext.keyframeAnimateTo(...)。
3.1 基本用法结构
this.uiContext?.keyframeAnimateTo(
{ iterations: 1 }, // 整体参数
[
{ duration: 400, curve: Curve.EaseInOut, event: () => { ... } },
{ duration: 300, curve: Curve.Linear, event: () => { ... } },
// 还可以继续加
]
);
3.2 实战例子:弹跳徽标
import { UIContext } from '@kit.ArkUI';
@Entry
@Component
struct KeyframeDemo {
@State scale: number = 1;
@State offsetY: number = 0;
uiContext?: UIContext;
aboutToAppear() {
this.uiContext = this.getUIContext?.();
}
play() {
if (!this.uiContext) return;
this.uiContext.keyframeAnimateTo(
{ iterations: 1 },
[
{
duration: 200,
curve: Curve.EaseOut,
event: () => {
this.scale = 1.3;
this.offsetY = -10;
}
},
{
duration: 150,
curve: Curve.EaseIn,
event: () => {
this.scale = 0.9;
this.offsetY = 5;
}
},
{
duration: 200,
curve: Curve.EaseOut,
event: () => {
this.scale = 1.0;
this.offsetY = 0;
}
}
]
);
}
build() {
Column() {
Text('⭐')
.fontSize(50)
.scale({ x: this.scale, y: this.scale })
.translate({ y: this.offsetY })
.animation({ duration: 50 }) // 每段之间的小过渡更顺
Button('播放关键帧动画').onClick(() => this.play())
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
重点:
- 每个关键帧
duration是该段动画的时长 event里只负责“目标状态”- 系统自动帮你补插值和过渡
四、AnimatedProperty & 动画控制器:想精细控制,就别只用 @State
4.1 “可动画属性”本质是啥?
ArkUI 把属性分成两类:可动画 / 不可动画。
-
可动画属性:
- 改变会引发 UI 变化
- 变化“适合”用动画过渡
- 包括:宽高、位置、缩放、旋转、透明度、圆角、阴影、背景色、文字大小等等
-
不可动画属性:
- 例如:
focusable、zIndex等,不适合慢慢变,那就不要加动画。
- 例如:
更高级的玩法是:
用
@AnimatableExtend给自定义绘制内容抽象出“可动画属性”,比如音量柱高度之类。
4.2 用 @AnimatableExtend 扩展可动画属性(概念级)
@AnimatableExtend
class GaugeAnim {
value: number = 0; // 0 ~ 1 表示进度
}
然后你就可以把 GaugeAnim 作为一个可动画属性,在每一帧里用它来绘制自定义 Canvas——详细实现这里不展开,只要你知道 ArkUI 支持自定义可动画属性就够了。
4.3 低层动画控制器:Animator
当 .animation() 和 animateTo 不够用了(比如需要暂停 / 继续 / 倒放),可以下探到 @ohos.animator。
import { Animator, AnimatorOptions, AnimatorResult } from '@ohos.animator';
@Entry
@Component
struct AnimatorDemo {
@State value: number = 0;
anim?: AnimatorResult;
aboutToAppear() {
const opt: AnimatorOptions = {
duration: 1000,
easing: 'friction',
begin: 0,
end: 100,
iterations: -1
};
this.anim = Animator.create(opt);
this.anim?.onframe = (v: number) => {
this.value = v;
};
}
build() {
Column() {
Text(`当前值:${this.value.toFixed(0)}`)
.fontSize(30)
Row() {
Button('Play').onClick(() => this.anim?.play());
Button('Pause').onClick(() => this.anim?.pause());
Button('Stop').onClick(() => this.anim?.finish());
}.margin({ top: 20 }).space(12)
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
这类 Animator 更偏“时间轴驱动”,适合:
- 某些不直接绑定组件属性的动画(比如数值变化、音频可视化)
- 需要精细播放控制(暂停、继续、加速、倒放等)
五、组合动画:一个交互往往不只一个属性在动
一个“看起来高级”的动画,往往是多个属性 / 多个组件的组合。
5.1 同一组件多个属性组合
示例:卡片点击 → 放大 + 提升阴影 + 轻微上移
@Entry
@Component
struct ComboCard {
@State scale: number = 1;
@State offsetY: number = 0;
@State shadowBlur: number = 4;
build() {
Column() {
Column() {
Text('组合动画卡片').fontSize(20)
}
.scale({ x: this.scale, y: this.scale })
.translate({ y: this.offsetY })
.shadow({
radius: this.shadowBlur,
color: Color.fromARGB(80, 0, 0, 0)
})
.backgroundColor(Color.White)
.borderRadius(16)
.padding(20)
.animation({ duration: 250, curve: Curve.EaseOut })
.onTouch((e) => {
if (e.type === TouchType.Down) {
this.scale = 1.03;
this.offsetY = -4;
this.shadowBlur = 12;
} else if (e.type === TouchType.Up || e.type === TouchType.Cancel) {
this.scale = 1;
this.offsetY = 0;
this.shadowBlur = 4;
}
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('#F5F5F5')
}
}
整个交互没有显式 animateTo,只靠 .animation() + 改状态就能完成。
5.2 多组件组合:按钮点击,多个元素一起动
这种玩法很适合做“结果反馈”:
- 按钮缩小 / 变色
- 图标飞出去
- 背景淡入遮罩
可以用一个 animateTo 把所有涉及状态改掉,在闭包外各自绑定动画。
六、手势联动动画:让动画“跟着手走”
没有什么比“跟手动画”更能提升高级感了。
6.1 Bottom Sheet 联动:拖动高度 + 松手自动收 / 展
@Entry
@Component
struct BottomSheetDemo {
private readonly MIN_HEIGHT: number = 80;
private readonly MAX_HEIGHT: number = 400;
@State sheetHeight: number = 80;
build() {
Stack() {
// 背景内容
Column() {
Text('主内容区').fontSize(24)
}.width('100%').height('100%').backgroundColor('#EEEEEE')
// Bottom Sheet
Column() {
Row() {
Text('上拉查看更多').fontSize(16)
}
.height(40).justifyContent(FlexAlign.Center)
// 这里放列表、操作区域等
Text('这里是 Sheet 内容').margin(16)
}
.width('100%')
.height(this.sheetHeight)
.backgroundColor(Color.White)
.borderRadius({ topLeft: 16, topRight: 16 })
.align(Alignment.Bottom)
.gesture(
PanGesture()
.onActionUpdate((e) => {
// 跟手拖动:手指向上,sheetHeight 变大
let next = this.sheetHeight - e.offsetY; // offsetY 向上为负
this.sheetHeight = Math.min(this.MAX_HEIGHT, Math.max(this.MIN_HEIGHT, next));
})
.onActionEnd(() => {
// 松手后自动吸附到“展开 / 收起”
const mid = (this.MAX_HEIGHT + this.MIN_HEIGHT) / 2;
const target = this.sheetHeight > mid ? this.MAX_HEIGHT : this.MIN_HEIGHT;
this.getUIContext()?.animateTo(
{ duration: 250, curve: Curve.EaseOut },
() => this.sheetHeight = target
);
})
)
}
.width('100%')
.height('100%')
}
}
这里有几个点可以记一下:
- 手势更新时 不要用 animateTo,直接改状态,保证“跟手”;
- 手势结束时再用
animateTo做一个收尾弹回或吸附; - 限制最小 / 最大高度防止拖“飞”。
6.2 手势 + 动画:卡片滑动删除(侧滑 + 离场动效)
思路是:
PanGesture更新offsetX- 超过阈值后,
animateTo把整个卡片位移出屏幕,配合transition做删除
这里就不把全代码铺开了,但你可以联想一下:
- 水平拖动 → translateX
- 松手时判断
offsetX - 超过 1/3 宽度就认为删除
七、常见 UI 动效案例:这些是项目里真会用到的
最后,用几个“非常接地气”的动效做收尾。
7.1 按钮点击压缩反馈
@Component
struct PressButton {
@State scale: number = 1;
build() {
Button('提交')
.scale({ x: this.scale, y: this.scale })
.animation({ duration: 80, curve: Curve.EaseOut })
.onTouch((e) => {
if (e.type === TouchType.Down) {
this.scale = 0.95;
} else if (e.type === TouchType.Up || e.type === TouchType.Cancel) {
this.scale = 1;
}
})
}
}
用户会下意识觉得:“这个按钮是活的”。
7.2 点赞 / 收藏心形动效(放大 + 弹回)
@Component
struct LikeIcon {
@State liked: boolean = false;
@State scale: number = 1;
play() {
this.getUIContext()?.keyframeAnimateTo(
{},
[
{ duration: 120, event: () => this.scale = 1.4 },
{ duration: 120, event: () => this.scale = 0.9 },
{ duration: 120, event: () => this.scale = 1.0 }
]
);
}
build() {
Text(this.liked ? '❤️' : '🤍')
.fontSize(32)
.scale({ x: this.scale, y: this.scale })
.animation({ duration: 80 })
.onClick(() => {
this.liked = !this.liked;
this.play();
})
}
}
比单纯变色手感好太多。
7.3 列表新增 / 删除项:transition 组件转场
根据性能建议,组件插入 / 删除优先用 transition,少用手动 animateTo 重写布局。
@Entry
@Component
struct ListTransitionDemo {
@State items: number[] = [1, 2, 3, 4];
build() {
Column() {
Button('添加一行').onClick(() => {
this.getUIContext()?.animateTo({ duration: 200 }, () => {
this.items.push(Date.now());
});
})
Button('删除最后一行').onClick(() => {
this.getUIContext()?.animateTo({ duration: 200 }, () => {
this.items.pop();
});
}).margin({ top: 8 })
ForEach(this.items, (it) => {
Row() {
Text(`Item ${it}`).fontSize(18)
}
.height(40)
.width('90%')
.backgroundColor('#FFFFFF')
.borderRadius(8)
.margin({ top: 6 })
.transition({
type: TransitionType.All,
opacity: 0,
translate: { x: 30, y: 0 }
})
}, (it) => it.toString())
}
.width('100%')
.padding(16)
.backgroundColor('#F2F2F2')
}
}
插入时:从右侧淡入;
删除时:淡出 + 左移(是逆过程,ArkUI 会自动反向补帧)。
最后:动效不是“越多越炫”,而是“刚刚好”
写到这里,其实你已经掌握了 ArkUI 动画系统 80% 的实战能力:
- 知道什么时候用
animateTo,什么时候用.animation(); - 知道复杂多段动画可以交给
keyframeAnimateTo; - 知道需要暂停 / 播放 / 调速时,可以下探到 Animator;
- 知道转场动画要用
transition,别自己造轮子; - 知道手势联动不要每一帧都 animateTo,而是跟手修改 + 松手收尾。
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐


所有评论(0)