动画基础:属性动画(animationTo)与显式动画(28)
在 HarmonyOS 的 ArkUI 框架中,动画是提升应用交互体验的关键。首先需要澄清一个概念:ArkUI 中用于驱动状态变化触发布局更新的显式动画接口标准名称为 animateTo(而非 animationTo)。
ArkUI 提供了完整的动画系统,其中属性动画与显式动画是最核心的两种类型。它们在实现机制和适用场景上各有侧重:
一、 属性动画(animation)
属性动画是最基础易懂的动画类型。当组件的某些通用属性(如 width、height、backgroundColor、opacity、scale、rotate、translate 等)发生变化时,可以通过它实现渐变过渡效果。
核心机制:
开发者无需使用闭包,只需在组件的属性后链式调用 animation() 接口。只要系统检测到其绑定的可动画属性发生变化,就会自动根据配置的动画参数(如时长、曲线)添加过渡动画。
适用场景:
适用于对多个可动画属性配置不同参数动画的场景,或者处理简单的状态切换(如按钮点击、悬停反馈)。
在使用 animation() 接口时,必须严格注意以下两点:
- 只对上方属性生效:
animation接口只对写在它前面的属性生效,且对组件构造器的属性(如Column({ space: this.space }))不生效。 - 支持多组独立动画:由于组件的属性链式调用是从下往上执行的,因此您可以根据调用顺序,对同一个组件的多个属性设置完全不同的动画参数。
代码示例(多属性差异化动画):
Text('ArkUI')
.backgroundColor(0xf56c6c)
.rotate({ angle: this.rotateValue }) // 属性1
.translate({ x: this.translateXX }) // 属性2
.animation({ // 作用于上方的 translate
curve: curves.springMotion(),
duration: 300
})
.rotate({ angle: this.rotateValuee }) // 属性3
.animation({ // 作用于上方的 rotate
curve: curves.springMotion(),
duration: 1200
})
性能优化:动画属性的性能梯队
在 PC 端或包含大量元素的复杂场景中,不同属性的动画开销差异巨大。建议遵循以下性能梯队进行开发:
- 第一梯队(最优):
opacity、translate、scale、rotate。这些变化只影响 GPU 层面的合成操作,无需重新计算布局,开销极小,即使上百个元素同时动画也基本不掉帧。 - 第二梯队(良好):
backgroundColor、borderColor等颜色属性。需要框架做颜色插值计算,但在常规设备下完全不是问题。 - 第三梯队(需注意):
width、height、margin、padding等布局属性。这些属性的变化会触发布局重计算(layout),如果同时有几十个元素改变宽高,极易导致帧率下降。
二、 显式动画(animateTo)
显式动画是 ArkUI 提供的全局接口,专门用于指定由于闭包代码导致的状态变化插入过渡动效。
核心机制:
与属性动画不同,animateTo 必须在一个回调函数(闭包)中执行。开发者需要在闭包内修改状态变量,框架会自动捕获这些变化,并为闭包前后的 UI 界面差异插值生成动画。
适用场景:
适用于需要对多个可动画属性配置相同动画参数的场景、需要嵌套使用动画的场景,以及需要精细控制延迟、级联动画和完成回调的复杂动画编排。
基础属性变化与多属性联动
这是最常用的场景,通过点击按钮触发多个属性的同时变化,且闭包内的所有状态变化都会遵循相同的动画参数。
@Entry
@Component
struct BasicAnimateToExample {
@State animate: boolean = false;
@State rotateValue: number = 0;
@State translateX: number = 0;
@State opacityValue: number = 1;
build() {
Row() {
Column()
.width(100)
.height(100)
.backgroundColor('#317AF7')
.borderRadius(30)
.rotate({ angle: this.rotateValue })
.translate({ x: this.translateX })
.opacity(this.opacityValue)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.animate = !this.animate;
// 核心:使用 getUIContext() 明确 UI 执行上下文
this.getUIContext()?.animateTo({
duration: 1000,
curve: Curve.EaseInOut
}, () => {
// 闭包内改变状态,系统会自动检测差异并添加过渡动画
this.rotateValue = this.animate ? 90 : 0;
this.translateX = this.animate ? 100 : 0;
this.opacityValue = this.animate ? 0.5 : 1;
});
})
}
}
实例二:组件出现时的入场动画
利用 onAppear 生命周期钩子,在组件首次挂载到界面时触发入场动效(注意:不能在 aboutToAppear 中调用)。
@Entry
@Component
struct AppearAnimateToExample {
@State rotateAngle: number = 0;
build() {
Column() {
Button('入场动画示例')
.margin(50)
.rotate({ x: 0, y: 0, z: 1, angle: this.rotateAngle })
.onAppear(() => {
// 组件出现时开始做动画
this.getUIContext()?.animateTo({
duration: 1200,
curve: Curve.Friction, // 阻尼曲线,效果更自然
delay: 200, // 延迟 200ms 执行
iterations: -1, // 设置为 -1 表示无限循环
playMode: PlayMode.Alternate // 正向播放后自动反向播放
}, () => {
this.rotateAngle = 90;
})
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
实例三:物理弹簧与手势跟手/离手衔接
在滑动交互中,跟手阶段使用 responsiveSpringMotion,离手后使用 springMotion 并传入当前速度,实现极具真实感的物理惯性效果。
import { curves } from '@kit.ArkUI';
@Entry
@Component
struct GestureSpringExample {
@State cardOffset: number = 0;
private lastCardOffset: number = 0;
build() {
Column() {
Button('拖拽我')
.translate({ x: this.cardOffset })
.gesture(
PanGesture()
.onActionUpdate((event: GestureEvent | undefined) => {
if (event) {
// 1. 跟手阶段:直接修改状态,配合响应弹簧曲线
this.getUIContext()?.animateTo({
curve: curves.responsiveSpringMotion()
}, () => {
this.cardOffset = this.lastCardOffset + event.offsetX;
});
}
})
.onActionEnd((event: GestureEvent | undefined) => {
if (event) {
// 2. 离手阶段:使用普通弹簧曲线,并传入当前速度实现惯性
this.getUIContext()?.animateTo({
curve: curves.springMotion(0.5, 0.8, event.velocity)
}, () => {
// 这里可以写回弹到原点或吸附到目标位置的逻辑
this.cardOffset = 0;
this.lastCardOffset = this.cardOffset;
});
}
})
)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
实例四:立即中断正在进行的动画
如果在动画播放过程中需要立刻停止并定格在某个状态,可以使用 duration: 0 配合属性重置。
@Entry
@Component
struct StopAnimateExample {
@State rotateAngle: number = 0;
build() {
Column() {
Button('停止旋转')
.margin(50)
.rotate({ x: 0, y: 0, z: 1, angle: this.rotateAngle })
.onClick(() => {
// 在 duration 为 0 的动画中修改属性
// 可以立即停止该属性之前的动画,并按新设置的属性显示
this.getUIContext()?.animateTo({ duration: 0 }, () => {
this.rotateAngle = 0;
})
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
三、 两者的核心区别
- 触发方式:属性动画是“被动触发”,只需在属性后绑定
animation(),状态改变即触发;显式动画是“主动触发”,必须显式调用animateTo并在闭包中修改状态。 - 作用域:
animation接口只会作用于在其之上的属性调用;而animateTo闭包内改变的任何可动画属性,都会遵循相同的动画参数。
四、 进阶特性:动画衔接
显式动画具备强大的动画衔接能力。当存在正在运行的动画时,如果用户行为(如连续快速点击)导致属性终点值发生变化,开发者仅需在 animateTo 动画闭包中再次改变属性值,系统会自动衔接之前的动画和当前的动画,平滑过渡到新的终点值,避免产生停顿感或骤变。
1、 高级动画曲线:物理弹簧曲线
除了常规的线性或缓入缓出曲线,ArkUI 提供了极具真实感的物理弹簧曲线,常用于列表滑动、弹窗弹出等需要惯性和阻尼效果的场景。
springMotion(离手惯性曲线):
适用于手指离开屏幕后的惯性滑动。它需要接收一个velocity(速度)参数,能够根据上一帧的手势速度,生成带有自然减速效果的动画。responsiveSpringMotion(跟手响应曲线):
适用于手指拖拽过程中的实时反馈。它响应极快,没有明显的延迟,能让 UI 像粘在手指上一样跟手。
代码示例:
// 跟手阶段:使用 responsiveSpringMotion
this.getUIContext().animateTo({
curve: curves.responsiveSpringMotion(0.5, 0.8)
}, () => {
this.translateX = event.offsetX;
});
// 离手阶段:使用 springMotion 并传入当前速度
this.getUIContext().animateTo({
curve: curves.springMotion(0.5, 0.8, currentVelocity)
}, () => {
this.translateX = targetX; // 吸附到目标位置
});
2、 转场动画(TransitionEffect)
当组件被创建(出现)或销毁(消失)时,可以使用 transition 属性为其添加出场/入场动画。
核心机制:
通过 TransitionEffect 组合不同的效果(如 opacity、translate、scale),并绑定到组件的 transition 属性上。当组件通过 if/else 或 ForEach 增删时,动画会自动触发。
代码示例:
if (this.isShow) {
Column() {
Text('弹窗内容')
}
.transition(
TransitionEffect.OPACITY // 透明度渐变
.combine(TransitionEffect.translate({ y: 100 })) // 组合Y轴平移
.animation({ duration: 300, curve: Curve.EaseOut })
)
}
3、 手势与动画的深度联动
在拖拽、缩放等复杂交互中,动画与手势是密不可分的。
最佳实践:
- 拖拽中(onActionUpdate):直接修改状态变量,配合
responsiveSpringMotion实现无延迟跟手。 - 拖拽结束(onActionEnd):获取
event.velocity,在animateTo中使用springMotion实现惯性滑动或回弹。 - 防抖与节流:在高频手势事件中,避免在
onActionUpdate中频繁调用animateTo,直接赋值即可;仅在onActionEnd中调用animateTo进行收尾动画。
4、 全局动画配置与性能监控
- 关闭全局动画:
在自动化测试或低端设备降级场景中,可以通过AppStorage或Configuration动态关闭全局动画,提升性能。// 在 EntryAbility 中配置 Configuration.setGlobalAnimationEnabled(false); - 动画性能监控:
使用onFinish回调配合hilog记录动画耗时。如果动画频繁掉帧,应检查是否触发了不必要的布局重排(如动画过程中改变了width/height),建议将重排动画替换为translate/scale/opacity等纯渲染层动画。
五、 开发注意事项
- UI 上下文依赖:
animateTo依赖 UI 的执行上下文,不可在 UI 上下文不明确的地方(如aboutToAppear或aboutToDisappear中)使用,建议通过getUIContext()?.animateTo来明确上下文。 - 布局类属性动画:对于改变布局类属性(如宽高)的动画,内容(如文字、Canvas)通常会直接跳转到最终状态。如果希望内容跟随宽高变化,可以使用
renderFit属性进行配置。 - 性能优化:在动画性能的优先级排序中,
opacity(透明度)优于transform(平移/缩放/旋转),优于backgroundColor,最后才是width/height。因为opacity是纯 GPU 合成层操作,不触发布局重排和重绘,是大规模列表动画的首选方案。
更多推荐


所有评论(0)