OpenHarmony 页面动画与组件动效全实战
·
一、前言
前面系列教程完成了鸿蒙 UI 组件、路由、本地存储、网络请求、自定义弹窗、RDB 数据库、动态权限全栈基础开发,而流畅动画是提升 APP 质感、优化用户交互体验的关键。
原生 ArkUI 内置高性能动画能力,无需依赖第三方库,支持四大核心动画场景:
- 属性动画(animateAttr):宽高、位移、透明度、圆角、缩放渐变,最常用组件动画
- 显式动画(animateTo):状态变量驱动动画,一行代码实现过渡动效,极简易用
- 弹窗入场 / 退场动画:搭配前文 CustomDialog,定制弹窗弹出收起动画
- 页面路由转场动画:页面跳转自定义切换动画,替换系统默认生硬切换
本文全部案例贴合实际项目,补齐整套鸿蒙开发教程最后一块交互短板,所有代码兼容最新 OpenHarmony 4.0/5.0 版本,可直接复制运行。
二、ArkUI 动画核心基础概念
2.1 动画通用参数说明
- duration:动画时长,单位 ms,建议 200-500ms,符合人体视觉习惯
- curve:动画插值曲线(线性、缓入、缓出、弹性曲线)
- delay:动画延迟执行时间
- iterations:动画循环次数,-1 代表无限循环
- playMode:动画播放模式,正向 / 反向 / 交替播放
2.2 两类动画核心区别(开发必记)
- animateTo 显式动画:操作简单,修改状态变量自动生成过渡动画,适合按钮点击、弹窗显隐、数值变化
- animateAttr 属性动画:细粒度更高,直接操作组件样式属性,无需状态变量,适合悬浮动画、呼吸灯、图标微动
三、入门实战:animateTo 显式动画(项目最常用)
核心逻辑:把状态修改代码放入 animateTo 回调,系统自动生成平滑过渡动画,零基础上手最简单。
3.1 基础案例:组件缩放 + 位移 + 透明度动画
ets
@Entry
@Component
struct AnimateToBasicDemo {
// 绑定动画状态变量
@State scaleSize: number = 1
@State offsetX: number = 0
@State opacityValue: number = 1
startAnim() {
// 开启显式动画
animateTo({
duration: 300, // 动画时长300ms
curve: Curve.EaseOut, // 先快后慢,贴合原生系统动画节奏
iterations: 1,
playMode: PlayMode.Normal
}, () => {
// 此处修改所有状态变量,自动附带过渡动画
this.scaleSize = 1.5
this.offsetX = 80
this.opacityValue = 0.4
})
}
resetAnim() {
animateTo({duration: 300}, () => {
this.scaleSize = 1
this.offsetX = 0
this.opacityValue = 1
})
}
build() {
Column({space:30}) {
// 带动画的方块组件
Rect()
.width(100)
.height(100)
.fillColor('#007DFF')
.scale({x:this.scaleSize, y:this.scaleSize})
.offset({x:this.offsetX})
.opacity(this.opacityValue)
Row({space:20}) {
Button("开启动画").onClick(()=>this.startAnim())
Button("重置动画").onClick(()=>this.resetAnim())
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
3.2 高阶案例:无限循环呼吸动画(图标常驻微动)
适用于首页 logo、加载图标、状态提示图标,无限循环淡入淡出呼吸效果
ets
@Entry
@Component
struct BreatheAnimDemo {
@State opacity: number = 0.6
aboutToAppear() {
// 页面加载自动开启无限呼吸动画
animateTo({
duration: 1500,
curve: Curve.EaseInOut,
iterations: -1, // 无限循环
playMode: PlayMode.Alternate // 往返播放
}, () => {
this.opacity = 1
})
}
build() {
Column() {
Image($r('app.media.icon'))
.width(120)
.opacity(this.opacity)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
四、进阶实战:animateAttr 属性动画(无状态变量动画)
无需定义 @State 状态变量,直接绑定组件,精准控制单一组件样式变化,适合复杂连续动画、悬浮按钮动效。
4.1 悬浮按钮上下浮动动画
ets
@Entry
@Component
struct FloatButtonAnim {
build() {
Column() {
Button("悬浮按钮")
.width(120)
.height(48)
.backgroundColor('#FF6734')
// 绑定属性动画
.animateAttr({
duration: 2000,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
// 纵向上下偏移浮动
.offset({y:20})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
4.2 旋转动画:加载转圈图标
ets
@Entry
@Component
struct RotateAnimDemo {
build() {
Column() {
Rect()
.width(80)
.height(80)
.fillColor('transparent')
.stroke({width:6, color:'#007DFF'})
.borderRadius(40)
// 无限旋转动画
.animateAttr({
duration: 1200,
curve: Curve.Linear, // 线性匀速旋转
iterations: -1
})
.rotate({angle:360})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
五、项目实战 1:自定义弹窗入场退场动画(联动前文 CustomDialog)
原生弹窗生硬无动画,这里给之前项目用到的自定义弹窗添加从上弹出、从底部滑入、渐入动画,直接无缝对接之前弹窗代码。
ets
// 带缩放+透明入场动画的自定义弹窗
@CustomDialog
struct AnimDialog {
controller: CustomDialogController
@DialogParam confirmClick:()=>void
build() {
Column({space:20}) {
Text("温馨提示").fontSize(20).fontWeight(FontWeight.Bold)
Text("当前操作不可撤销,是否继续?").fontSize(16)
Button("确认").width(120).onClick(()=>{
this.confirmClick()
this.controller.close()
})
}
.width(300)
.padding(24)
.backgroundColor(Color.White)
.borderRadius(16)
// 弹窗入场动画:缩放+透明过渡
.animateAttr({duration:250, curve:Curve.EaseOut})
.scale({x:0.8, y:0.8})
.opacity(0)
}
}
@Entry
@Component
struct DialogAnimPage {
dialogCtrl:CustomDialogController = new CustomDialogController({
builder:AnimDialog({confirmClick:()=>console.log("确认点击")}),
autoCancel:true
})
build() {
Column() {
Button("打开带动画弹窗").width(220).onClick(()=>{
this.dialogCtrl.open()
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
六、项目实战 2:页面路由自定义转场动画(路由跳转动画)
默认页面跳转生硬,支持左滑入场、底部弹出、渐隐切换三种主流页面转场动画,修改 router 路由配置即可。
ets
import router from '@ohos.router'
@Entry
@Component
struct AnimRouterIndex {
build() {
Column({space:25}) {
Text("首页页面").fontSize(24).fontWeight(FontWeight.Bold)
// 1. 左侧滑入动画(常规页面跳转)
Button("左滑跳转详情页").width(220).onClick(()=>{
router.pushUrl({
url:"pages/detail/detail",
transition:RouterTransition.SlideLeft
})
})
// 2. 底部弹出页面动画
Button("底部弹出页面").width(220).onClick(()=>{
router.pushUrl({
url:"pages/detail/detail",
transition:RouterTransition.SlideBottom
})
})
// 3. 渐隐切换页面
Button("渐隐切换页面").width(220).onClick(()=>{
router.pushUrl({
url:"pages/detail/detail",
transition:RouterTransition.Fade
})
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
路由全部内置转场动画枚举
- SlideLeft:页面向左推入(最常用,系统原生标准)
- SlideBottom:页面从底部向上弹出
- Fade:全局透明渐入渐出
- Scale:缩放页面切换
七、综合实战:列表条目侧滑删除动画(电商 / 资讯项目必备)
结合 List 列表 + 显式动画,实现 App 主流的列表项左滑显示删除按钮交互,完全复刻原生 APP 效果。
ets
@Entry
@Component
struct ListSlideAnimDemo {
@State listOffset: number = 0
build() {
List() {
ListItem() {
Stack() {
// 前置展示内容
Row() {
Text("订单列表条目").fontSize(16).layoutWeight(1)
}
.width('100%')
.height(70)
.padding(15)
.backgroundColor(Color.White)
.offset({x:this.listOffset})
// 右侧隐藏删除按钮
Row() {
Text("删除").fontColor(Color.White)
}
.width(80)
.height(70)
.backgroundColor('#f56c6c')
.position({x:'100%'})
}
}
// 点击触发侧滑动画
.onClick(()=>{
animateTo({duration:300, curve:Curve.EaseOut},()=>{
// 左滑展示删除按钮
this.listOffset = this.listOffset===0 ? -80 : 0
})
})
}
.width('100%')
.padding(15)
.backgroundColor('#F5F5F5')
}
}
八、动画开发最佳实践与避坑指南
8.1 动画时长规范(统一项目 UI 风格)
- 弹窗、按钮点击:200ms-300ms(轻快不拖沓)
- 页面转场:350ms(系统原生一致)
- 呼吸、悬浮循环动画:1500ms-2000ms
8.2 曲线选择建议
- 常规交互:Curve.EaseOut(先快后慢,最贴合手机原生)
- 匀速旋转加载:Curve.Linear
- 往返微动呼吸动画:Curve.EaseInOut
8.3 高频避坑点
- 动画内部禁止频繁修改大量状态变量,避免页面卡顿
- 长列表不要给每一个条目添加高频动画,防止性能雪崩
- 页面销毁时,不会自动终止无限循环动画,复杂页面建议在 aboutToDisappear 手动终止动画
- 路由转场动画仅支持 pushUrl 跳转,replaceUrl 不推荐搭配动画
更多推荐



所有评论(0)