显式动画
显示动画由全局方法 animateTo
实现,它主要是解决由于闭包代码导致的状态变化而插入的动画效果。
显式动画定义介绍
declare function animateTo(value: AnimateParam, event: () => void): void;
value:
设置动画的具体配置参数,AnimateParam 说明如下:
duration:设置动画的执行时长,单位为毫秒,默认为 1000 毫秒。
tempo:设置动画的播放速度,值越大动画播放越快,值越小播放越慢,默认值为 1 ,为 0 时无动画效果。
curve:设置动画曲线,默认值为 Linear 。
delay:设置动画的延迟执行时间,单位为毫秒,默认值为 0 不延迟。
iterations:设置动画的执行次数,默认值为 1 次,设置为 -1 时无限循环。
playMode:设置动画播放模式,默认播放完成后重头开始播放。
onFinish:动画播放完成的回调。
event:
指定显示动效的闭包函数,在闭包函数中导致的状态变化系统会自动插入过渡动画。
简单样例如下所示:
@Entry @Component struct Index {
@State btnWidth: number = 200;
@State btnHeight: number = 60;
@State btnAnim: boolean = true;
build() {
Column() {
Button('Click Me')
.size({width: this.btnWidth, height: this.btnHeight})
.onClick(() => {
if(this.btnAnim) {
animateTo({
duration: 1300,
tempo: 1,
curve: Curve.Sharp,
delay: 200,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
prompt.showToast({ message: "play finish" })
}
}, () => {
this.btnWidth = 100;
this.btnHeight = 50;
});
} else {
animateTo({
duration: 300,
tempo: 1,
curve: Curve.Linear,
delay: 200,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
prompt.showToast({ message: "play finish" })
}
}, () => {
this.btnWidth = 200;
this.btnHeight = 60;
});
}
this.btnAnim = !this.btnAnim;
})
}
.padding(10)
.size({width: "100%", height: '100%'})
}
}
运行结果如下图所示:
自定义单步动画
ArkUI 开发框架没有单步动画的定义,笔者根据动画执行的时机称之为单步动画,它指的在是一组复杂的动画流中,动画是依次执行的,待上一个动画执行完毕后才开始执行下一个动画。
接下来笔者结合组件的 translate() 方法演示一下使用显式动画 animateTo() 对控件做单个位移动画,演示样例如下所示:
@Entry @Component struct AnimateToTest {
@State translate: TranslateOptions = {
x: 0,
y: 0,
z: 0
}
build() {
Column({space: 10}) {
Button('animateTo')
.onClick(() => {
// 点击按钮,执行animateTo()方法
animateTo({
duration: 500,
tempo: 0.5,
curve: Curve.Linear,
delay: 100,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
// animateTo动画结束的回调
}
}, () => {
// 在闭包函数内改变translate状态,文本控件则执行位移动画
this.translate = {
x: 0,
y: 100,
z: 0
}
})
})
Text("AnimateTo")
.fontSize(20)
.backgroundColor("#aabbcc")
.translate(this.translate)
}
.width('100%')
.height('100%')
.padding(10)
}
}
样例运行结果如下图所示:
点击按钮后,执行全局 animateTo() 方法,animateTo() 方法的闭包函数内更改了 translate 的值,因此执行向下移动动画,如果向下移动动画结束后再执行向右移动动画,在向右移动动画结束后又执行向上移动动画等等,这就需要在上一个动画完整后再执行下一个动画……
针对这种场景,使用传统的回调嵌套可以实现,伪代码如下:
Button('animateTo')
.onClick(() => {
animateTo({
onFinish: () => {
// 动画结束执行下一个动画
animateTo({
onFinish: () => {
// 动画结束执行下一个动画
// 省略....
}
}, () => {})
}
}, () => {
// 省略
})
})
这种多层次次嵌套代码是及其不合理的,可以稍微做优化,把单个动画执行提取出一个方法,伪代码如下:
private animateStepX(event: () => void) {// 封装animateStepX方法
animateTo({
onFinish: () => {
event?.call(this);
}
}, () => {});
}
Button('animateTo')
.onClick(() => {
this.animateStepX(() => { // 调用animateStepX方法
this.animateStepX(() => { // 方法执行结束,调用下一步方法
// 省略....
})
})
})
根据伪代码看,虽然提取了 animateStepX()
方法出来,但是这种方式依然没有解决多层次嵌套的问题,对于这种场景,Promise
的实用性就体现出来了,它可以有效解决层级嵌套的问题,对 Promise
不熟悉的读者请自行了解一下。
利用 Promise
特性,封装一个单步动画方法,待每一步动画执行结束后再执行下一个动画,单步动画方法封装如下:
private animateStep(value: AnimateParam, event: () => void): () => Promise<boolean> {
return () => {
return new Promise((resolve, reject) => {
if(value) { // 判断参数是否合法
let onFinish = value.onFinish; // 保存原有动画回调
value.onFinish = () => { // 替换新的动画回调
onFinish?.call(this) // 执行原有动画回到
resolve(true); // 触发方法执行完毕
}
animateTo(value, event); // 执行动画
} else {
// reject("value invalid") // 触发方法执行失败
resolve(false); // 参数非法,跳过执行
}
});
}
}
animateStep()
方法参数和 animateTo()
方法参数一致,它返回的是一个新方法,新方法的返回类型为 Promise<boolean>
,这样就可以实现单个方法的执行控制了,使用样例如下所示:
@Entry @Component struct AnimateToTest {
@State translate: TranslateOptions = { // 位移数据
x: 0,
y: 0,
z: 0
}
private step1: () => Promise<boolean>; // 第一步动画
private step2: () => Promise<boolean>; // 第二步动画
private step3: () => Promise<boolean>; // 第三步动画
private step4: () => Promise<boolean>; // 第四步动画
build() {
Column({space: 10}) {
Button('animateTo')
.onClick(async () => {
await this.step1(); // 等待第一步动画执行完毕
await this.step2(); // 等待第二步动画执行完毕
await this.step3(); // 等待第三步动画执行完毕
await this.step4(); // 等待第四步动画执行完毕
})
Text("AnimateTo")
.fontSize(20)
.backgroundColor("#aabbcc")
.translate(this.translate)
}
.width('100%')
.height('100%')
.padding(10)
}
private animateStep(value: AnimateParam, event: () => void): () => Promise<boolean> {
return () => {
return new Promise((resolve, reject) => {
if(value) { // 判断参数是否合法
let onFinish = value.onFinish; // 保存原有动画回调
value.onFinish = () => { // 替换新的动画回调
onFinish?.call(this) // 执行原有动画回到
resolve(true); // 触发方法执行完毕
}
animateTo(value, event); // 开始执行显式动画
} else {
// reject("value invalid") // 触发方法执行失败
resolve(false); // 参数非法,不执行
}
});
}
}
aboutToAppear() {
let duration = 300;
this.step1 = this.animateStep({ // 初始化单步动画1
duration: duration,
tempo: 0.5,
curve: Curve.Linear,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
console.log("animation finish")
}
}, () => {
this.translate = {
x: 0,
y: 100,
z: 0
}
});
this.step2 = this.animateStep({ // 初始化单步动画2
duration: duration,
tempo: 0.5,
curve: Curve.Linear,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
console.log("animation finish")
}
}, () => {
this.translate = {
x: 100,
y: 100,
z: 0
}
});
this.step3 = this.animateStep({ // 初始化单步动画3
duration: duration,
tempo: 0.5,
curve: Curve.Linear,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
console.log("animation finish")
}
}, () => {
this.translate = {
x: 100,
y: 0,
z: 0
}
});
this.step4 = this.animateStep({ // 初始化单步动画4
duration: duration,
tempo: 0.5,
curve: Curve.Linear,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
console.log("animation finish")
}
}, () => {
this.translate = {
x: 0,
y: 0,
z: 0
}
});
}
// 提取一个方法
private generateAnimateParam(duration: number, event: () => void): AnimateParam {
return {
duration: duration,
tempo: 0.5,
curve: Curve.Linear,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
event?.call(this);
}
}
}
}
通过依次调用 step***() 方法,这样就构成了一个更为复杂连续的动画,样例运行结果如下所示: