动画是App中必不可少的部分,可以让应用效果更加炫酷。鸿蒙系统中提供了各种动画,下面一起看一下鸿蒙当中的动画:
属性动画
组件的某些通用属性变化时,可以通过属性动画实现渐变过度效果,提升用户体验。支持的属性包括width、height、backgroundColor、opacity、scale、rotate、translate等。布局类改变宽高的动画,内容都是直接到终点状态,例如文字、Canvas的内容等,如果要内容跟随宽高变化,可以使用renderFit属性配置。 代码如下:
// xxx.ets
@Entry
@Component
struct AttrAnimationExample {
@State widthSize: number = 250
@State heightSize: number = 100
@State rotateAngle: number = 0
@State flag: boolean = true
build() {
Column() {
Button('change size')
.onClick(() => {
if (this.flag) {
this.widthSize = 150
this.heightSize = 60
} else {
this.widthSize = 250
this.heightSize = 100
}
this.flag = !this.flag
})
.margin(30)
.width(this.widthSize)
.height(this.heightSize)
.animation({
duration: 2000,
curve: Curve.EaseOut,
iterations: 3,
playMode: PlayMode.Normal
})
Button('change rotate angle')
.onClick(() => {
this.rotateAngle = 90
})
.margin(50)
.rotate({ angle: this.rotateAngle })
.animation({
duration: 1200,
curve: Curve.Friction,
delay: 500,
iterations: -1, // 设置-1表示动画无限循环
playMode: PlayMode.Alternate,
expectedFrameRateRange: {
min: 20,
max: 120,
expected: 90,
}
})
}.width('100%').margin({ top: 20 })
}
}
显式动画
提供全局animateTo显式动画接口来制定由于闭包代码导致的状态变化插入过度动效。同属性动画,布局类改变宽高的动画,内容都是直接到终点状态,例如文字、Canvas的内容等,如果要内容随着宽高变化,可以使用renderFit属性配置。
代码如下:
// xxx.ets
@Entry
@Component
struct AnimateToExample {
@State widthSize: number = 250
@State heightSize: number = 100
@State rotateAngle: number = 0
private flag: boolean = true
build() {
Column() {
Button('change size')
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(() => {
if (this.flag) {
// 建议使用this.getUIContext()?.animateTo()
animateTo({
duration: 2000,
curve: Curve.EaseOut,
iterations: 3,
playMode: PlayMode.Normal,
onFinish: () => {
console.info('play end')
}
}, () => {
this.widthSize = 150
this.heightSize = 60
})
} else {
// 建议使用this.getUIContext()?.animateTo()
animateTo({}, () => {
this.widthSize = 250
this.heightSize = 100
})
}
this.flag = !this.flag
})
Button('stop rotating')
.margin(50)
.rotate({ x: 0, y: 0, z: 1, angle: this.rotateAngle })
.onAppear(() => {
// 组件出现时开始做动画
// 建议使用this.getUIContext()?.animateTo()
animateTo({
duration: 1200,
curve: Curve.Friction,
delay: 500,
iterations: -1, // 设置-1表示动画无限循环
playMode: PlayMode.Alternate,
expectedFrameRateRange: {
min: 10,
max: 120,
expected: 60,
}
}, () => {
this.rotateAngle = 90
})
})
.onClick(() => {
// 建议使用this.getUIContext()?.animateTo()
animateTo({ duration: 0 }, () => {
// this.rotateAngle之前为90,在duration为0的动画中修改属性,可以停止该属性之前的动画,按新设置的属性显示
this.rotateAngle = 0
})
})
}.width('100%').margin({ top: 5 })
}
}
关键帧动画
在UIContext中提供keyframeAnimateTo接口来指定若干个关键帧状态,实现分段的动画。同属性动画,布局类改变宽高的动画,内容都是直接到终点状态,例如文字、Canvas的内容等,如果要内容跟随宽高变化,可以使用renderFit属性配置。
代码如下:
// xxx.ets
import { UIContext } from '@kit.ArkUI';
@Entry
@Component
struct KeyframeDemo {
@State myScale: number = 1.0;
uiContext: UIContext | undefined = undefined;
aboutToAppear() {
this.uiContext = this.getUIContext?.();
}
build() {
Column() {
Circle()
.width(100)
.height(100)
.fill("#46B1E3")
.margin(100)
.scale({ x: this.myScale, y: this.myScale })
.onClick(() => {
if (!this.uiContext) {
console.info("no uiContext, keyframe failed");
return;
}
this.myScale = 1;
// 设置关键帧动画整体播放3次
this.uiContext.keyframeAnimateTo({ iterations: 3 }, [
{
// 第一段关键帧动画时长为800ms,scale属性做从1到1.5的动画
duration: 800,
event: () => {
this.myScale = 1.5;
}
},
{
// 第二段关键帧动画时长为500ms,scale属性做从1.5到1的动画
duration: 500,
event: () => {
this.myScale = 1;
}
}
]);
})
}.width('100%').margin({ top: 5 })
}
}
页面间转场
当路由进行切换时,可以通过在pageTransition函数中自定义页面入场和页面退场的转场动效。
代码如下:
// index.ets
import { router } from '@kit.ArkUI';
@Entry
@Component
struct Index {
@State scale1: number = 1
@State opacity1: number = 1
build() {
Column() {
Image($r("app.media.transition_image1")).width('100%').height('100%')
}
.width('100%')
.height('100%')
.scale({ x: this.scale1 })
.opacity(this.opacity1)
.onClick(() => {
router.pushUrl({ url: 'pages/Page1' })
})
}
pageTransition() {
PageTransitionEnter({ duration: 1200, curve: Curve.Linear })
.onEnter((type: RouteType, progress: number) => {
if (type == RouteType.Push||type == RouteType.Pop) {
this.scale1 = progress
this.opacity1 = progress
}
})
PageTransitionExit({ duration: 1200, curve: Curve.Ease })
.onExit((type: RouteType, progress: number) => {
if (type == RouteType.Push) {
this.scale1 = 1 - progress
this.opacity1 = 1 - progress
}
})
}
}
组件内转场
组件内转场主要通过transition属性配置转场参数,在组件插入和删除时显示过度动效,主要用于容器组件中的子组件插入和删除时,提升用户体验。
代码如下:
// xxx.ets
@Entry
@Component
struct TransitionEffectExample1 {
@State flag: boolean = true;
@State show: string = 'show';
build() {
Column() {
Button(this.show).width(80).height(30).margin(30)
.onClick(() => {
// 点击Button控制Image的显示和消失
if (this.flag) {
this.show = 'hide';
} else {
this.show = 'show';
}
this.flag = !this.flag;
})
if (this.flag) {
// Image的显示和消失配置为相同的过渡效果(出现和消失互为逆过程)
// 出现时从指定的透明度为0、绕z轴旋转180°的状态,变为默认的透明度为1、旋转角为0的状态,透明度与旋转动画时长都为2000ms
// 消失时从默认的透明度为1、旋转角为0的状态,变为指定的透明度为0、绕z轴旋转180°的状态,透明度与旋转动画时长都为2000ms
Image($r('app.media.testImg')).width(200).height(200)
.transition(TransitionEffect.OPACITY.animation({ duration: 2000, curve: Curve.Ease }).combine(
TransitionEffect.rotate({ z: 1, angle: 180 })
))
}
}.width('100%')
}
}
共享元素转场
当路由进行切换时,可以通过设置组件的sharedTransition属性将元素标记为共享元素并设置对应的共享元素转场动效。
// xxx.ets
@Entry
@Component
struct SharedTransitionExample {
@State active: boolean = false
build() {
Column() {
Navigator({ target: 'pages/PageB', type: NavigationType.Push }) {
Image($r('app.media.ic_health_heart')).width(50).height(50)
.sharedTransition('sharedImage', { duration: 800, curve: Curve.Linear, delay: 100 })
}.padding({ left: 20, top: 20 })
.onClick(() => {
this.active = true
})
}
}
}
组件内隐式共享元素转场
在视图切换过程中提供丝滑的上下文传承过渡。通过transition机制提供了opacity、scale等转场效果,geometryTransition通过安排绑定的in/out组件(in指新视图、out指旧视图)的frame、position使得原本独立的transition动画在空间位置上发生联系,将视觉焦点由旧视图位置引导到新视图位置。
代码如下:
// xxx.ets
@Entry
@Component
struct Index {
@State isShow: boolean = false
build() {
Stack({ alignContent: Alignment.Center }) {
if (this.isShow) {
Image($r('app.media.pic'))
.autoResize(false)
.clip(true)
.width(300)
.height(400)
.offset({ y: 100 })
.geometryTransition("picture", { follow: false })
.transition(TransitionEffect.OPACITY)
} else {
// geometryTransition此处绑定的是容器,那么容器内的子组件需设为相对布局跟随父容器变化,
// 套多层容器为了说明相对布局约束传递
Column() {
Column() {
Image($r('app.media.icon'))
.width('100%').height('100%')
}.width('100%').height('100%')
}
.width(80)
.height(80)
// geometryTransition会同步圆角,但仅限于geometryTransition绑定处,此处绑定的是容器
// 则对容器本身有圆角同步而不会操作容器内部子组件的borderRadius
.borderRadius(20)
.clip(true)
.geometryTransition("picture")
// transition保证组件离场不被立即析构,可设置其他转场效果
.transition(TransitionEffect.OPACITY)
}
}
.onClick(() => {
animateTo({ duration: 1000 }, () => {
this.isShow = !this.isShow
})
})
}
}
路径动画:
设置组件进行位移动画时的运动路径
代码如下:
// xxx.ets
@Entry
@Component
struct MotionPathExample {
@State toggle: boolean = true
build() {
Column() {
Button('click me').margin(50)
// 执行动画:从起点移动到(300,200),再到(300,500),再到终点
.motionPath({ path: 'Mstart.x start.y L300 200 L300 500 Lend.x end.y', from: 0.0, to: 1.0, rotatable: true })
.onClick(() => {
animateTo({ duration: 4000, curve: Curve.Linear }, () => {
this.toggle = !this.toggle // 通过this.toggle变化组件的位置
})
})
}.width('100%').height('100%').alignItems(this.toggle ? HorizontalAlign.Start : HorizontalAlign.Center)
}
}
粒子动画
粒子动画是在一定范围内随机生成的大量粒子产生运动而组成的动画。动画元素使一个个粒子,这些粒子可以是圆点、图片。通过对粒子在元素、透明度、大小、速度、加速度、自旋角度等维度变化做动画。来营造一种氛围感,比如下雪动效、雪花飘舞就相当于一个个雪花粒子在做动画。
代码如下:
// xxx.ets
@Entry
@Component
struct ParticleExample {
build() {
Stack() {
Text()
.width(300).height(300).backgroundColor(Color.Black)
Particle({particles:[
{
emitter:{
particle:{
type:ParticleType.POINT,//粒子类型
config:{
radius:10//圆点半径
},
count: 500,//粒子总数
lifetime:10000,//粒子生命周期,单位ms
lifetimeRange:100//粒子生命周期取值范围,单位ms
},
emitRate:10,//每秒发射粒子数
position:[0,0],
shape:ParticleEmitterShape.RECTANGLE//发射器形状
},
color:{
range:[Color.Red,Color.Yellow],//初始颜色范围
updater:{
type:ParticleUpdater.CURVE,//变化方式为曲线变化
config:[
{
from:Color.White,//变化起始值
to:Color.Pink,//变化终点值
startMillis:0,//开始时间
endMillis:3000,//结束时间
curve:Curve.EaseIn//变化曲线
},
{
from:Color.Pink,
to:Color.Orange,
startMillis:3000,
endMillis:5000,
curve:Curve.EaseIn
},
{
from:Color.Orange,
to:Color.Pink,
startMillis:5000,
endMillis:8000,
curve:Curve.EaseIn
},
]
}
},
opacity:{
range:[0.0,1.0],//粒子透明度的初始值从【0.0到1.0】随机产生
updater:{
type:ParticleUpdater.CURVE,
config:[
{
from:0.0,
to:1.0,
startMillis:0,
endMillis:3000,
curve:Curve.EaseIn
},
{
from:1.0,
to:0.0,
startMillis:5000,
endMillis:10000,
curve:Curve.EaseIn
}
]
}
},
scale:{
range:[0.0,0.0],
updater:{
type:ParticleUpdater.CURVE,
config:[
{
from:0.0,
to:0.5,
startMillis:0,
endMillis:3000,
curve: Curve.EaseIn
}
]
}
},
acceleration:{//加速度的配置,从大小和方向两个维度变化,speed表示加速度大小,angle表示加速度方向
speed:{
range:[3,9],
updater:{
type:ParticleUpdater.RANDOM,//Speed的变化方式是随机变化
config:[1,20]
}
},
angle:{
range:[90,90]
}
}
}
]
}).width(300).height(300)
}.width("100%").height("100%").align(Alignment.Center)
}
}
显示动画立即下发
animateToImmediately接口用来提供显示动画立即下发功能。同时加载多个属性动画的情况下,使用该接口可以立即执行闭包代码中状态变化导致的过渡动效。
代码如下:
// xxx.ets
@Entry
@Component
struct AnimateToImmediatelyExample {
@State widthSize: number = 250
@State heightSize: number = 100
@State opacitySize: number = 0
private flag: boolean = true
build() {
Column() {
Column()
.width(this.widthSize)
.height(this.heightSize)
.backgroundColor(Color.Green)
.opacity(this.opacitySize)
Button('change size')
.margin(30)
.onClick(() => {
if (this.flag) {
animateToImmediately({
delay: 0,
duration: 1000
}, () => {
this.opacitySize = 1
})
animateTo({
delay: 1000,
duration: 1000
}, () => {
this.widthSize = 150
this.heightSize = 60
})
} else {
animateToImmediately({
delay: 0,
duration: 1000
}, () => {
this.widthSize = 250
this.heightSize = 100
})
animateTo({
delay: 1000,
duration: 1000
}, () => {
this.opacitySize = 0
})
}
this.flag = !this.flag
})
}.width('100%').margin({ top: 5 })
}
}