HarmonyOS鸿蒙Next中在animateTo的onFinish里嵌套animateTo是不是可以代替keyframeAnimateTo
HarmonyOS鸿蒙Next中在animateTo的onFinish里嵌套animateTo是不是可以代替keyframeAnimateTo 感觉这样用功能比keyframeAnimateTo更自由,有什么坏处吗?
【背景知识】
- UIContext提供animateTo接口来指定由于闭包代码导致的状态变化插入过渡动效。
- 在UIContext中同样提供keyframeAnimateTo接口来指定若干个关键帧状态,实现分段的动画。同属性动画,布局类改变宽高的动画,内容都是直接到终点状态,例如文字、Canvas的内容等,如果要内容跟随宽高变化,可以使用renderFit属性配置。
【解决方案】
- 以下是一个keyframeAnimateTo关键帧动画的使用案例,代码如下:
import CryptoJS from '@ohos/crypto-js'
const NUMBER_OF_ITEMS = 20;
const MIN_ANM_DURATION = 120;
const MAX_ANM_DURATION = 180;
const MIN_OFFSET_X = -1;
const MAX_OFFSET_X = 5;
@Entry
@Component
struct Index {
@State num: number = 0
build() {
Column() {
Image($r('app.media.startIcon'))
.width(36)
.height(36)
.onClick(() => {
this.num++
})
FloatLikeView({ clickSum: this.num })
}.margin({
top: 500,
left: 200,
bottom: 0,
right: 0
})
}
}
@Component
struct FloatLikeView {
uiContext: UIContext | undefined = undefined;
private images: Array<Resource> = [$r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon'),
$r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon'),
$r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon'),
$r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon')];
@State scaleArray: Array<number> = [];
@State offXArray: Array<number> = [];
@State offYArray: Array<number> = [];
@State opacityArray: Array<number> = [];
@State imageViews: Array<number> = [];
@State flag: Array<boolean> = [];
@Prop @Watch('startAnimation') clickSum: number = 0;
aboutToAppear(): void {
this.uiContext = this.getUIContext?.();
for (let i = 0; i < NUMBER_OF_ITEMS; i++) {
this.imageViews.push(i);
this.opacityArray.push(0)
this.offXArray.push(0)
this.offYArray.push(0)
this.scaleArray.push(0)
this.flag.push(false)
}
}
getRandomInt(min: number, max: number): number {
// 使用安全随机数
return Math.floor(CryptoJS.lib.WordArray.random(1).words[0] / 0x100000000 * (max - min + 1)) + min;
}
build() {
Stack() {
ForEach(this.imageViews, (image: number, index: number) => {
Image(this.images[image % this.images.length])
.objectFit(ImageFit.Cover)
.width(36)
.height(36)
.opacity(this.opacityArray[index])
.scale({ x: this.scaleArray[index], y: this.scaleArray[index] })
.offset({
x: this.offXArray[index],
y: this.offYArray[index]
})
}, (image: Resource) => `${image.id}`)
}
}
startAnimation() {
this.clickAnimation()
}
clickAnimation() {
if (!this.uiContext) {
return;
}
let index = this.getRandomInt(0, NUMBER_OF_ITEMS);
index = index >= NUMBER_OF_ITEMS ? 0 : index;
while (this.flag[index] === true) {
index = this.getRandomInt(0, NUMBER_OF_ITEMS);
index = index >= NUMBER_OF_ITEMS ? 0 : index;
}
this.flag[index] = true;
this.uiContext.keyframeAnimateTo({
iterations: 1, onFinish: () => {
this.flag[index] = false;
}
}, [
{
duration: 1,
curve: Curve.Smooth,
event: () => {
this.scaleArray[index] = 0;
this.opacityArray[index] = 0;
this.offYArray[index] = 0;
this.offXArray[index] = 0;
}
},
{
duration: this.getRandomInt(MIN_ANM_DURATION, MAX_ANM_DURATION),
curve: Curve.Smooth,
event: () => {
this.scaleArray[index] = 0.3;
this.opacityArray[index] = 0.5;
this.offYArray[index] = -15;
this.offXArray[index] = this.getRandomInt(MIN_OFFSET_X, MAX_OFFSET_X);
}
},
{
duration: this.getRandomInt(MIN_ANM_DURATION, MAX_ANM_DURATION),
curve: Curve.Smooth,
event: () => {
this.scaleArray[index] = 0.5;
this.opacityArray[index] = 0.8;
this.offYArray[index] = -30;
this.offXArray[index] = this.getRandomInt(MIN_OFFSET_X, MAX_OFFSET_X);
}
},
{
duration: this.getRandomInt(MIN_ANM_DURATION, MAX_ANM_DURATION),
curve: Curve.Smooth,
event: () => {
this.scaleArray[index] = 0.6;
this.opacityArray[index] = 0.9;
this.offYArray[index] = -45;
this.offXArray[index] = this.getRandomInt(MIN_OFFSET_X, MAX_OFFSET_X);
}
},
])
}
}
- 若想通过在animateTo的onFinish回调方法嵌套animateTo接口实现与之相同的功能,可以参考如下案例:
import CryptoJS from '@ohos/crypto-js'
const NUMBER_OF_ITEMS = 20;
const MIN_ANM_DURATION = 120;
const MAX_ANM_DURATION = 180;
const MIN_OFFSET_X = -1;
const MAX_OFFSET_X = 5;
@Entry
@Component
struct Index {
@State num: number = 0
build() {
Column() {
Image($r('app.media.startIcon'))
.width(36)
.height(36)
.onClick(() => {
this.num++
})
FloatLikeView({ clickSum: this.num })
}.margin({
top: 500,
left: 200,
bottom: 0,
right: 0
})
}
}
@Component
struct FloatLikeView {
uiContext: UIContext | undefined = undefined;
private images: Array<Resource> = [$r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon'),
$r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon'),
$r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon'),
$r('app.media.startIcon'), $r('app.media.startIcon'), $r('app.media.startIcon')];
@State scaleArray: Array<number> = [];
@State offXArray: Array<number> = [];
@State offYArray: Array<number> = [];
@State opacityArray: Array<number> = [];
@State imageViews: Array<number> = [];
@State flag: Array<boolean> = [];
@Prop @Watch('startAnimation') clickSum: number = 0;
aboutToAppear(): void {
this.uiContext = this.getUIContext?.();
for (let i = 0; i < NUMBER_OF_ITEMS; i++) {
this.imageViews.push(i);
this.opacityArray.push(0)
this.offXArray.push(0)
this.offYArray.push(0)
this.scaleArray.push(0)
this.flag.push(false)
}
}
getRandomInt(min: number, max: number): number {
// 使用安全随机数
return Math.floor(CryptoJS.lib.WordArray.random(1).words[0] / 0x100000000 * (max - min + 1)) + min;
}
build() {
Stack() {
ForEach(this.imageViews, (image: number, index: number) => {
Image(this.images[image % this.images.length])
.objectFit(ImageFit.Cover)
.width(36)
.height(36)
.opacity(this.opacityArray[index])
.scale({ x: this.scaleArray[index], y: this.scaleArray[index] })
.offset({
x: this.offXArray[index],
y: this.offYArray[index]
})
}, (image: Resource) => `${image.id}`)
}
}
startAnimation() {
this.clickAnimation()
}
clickAnimation() {
if (!this.uiContext) {
return;
}
let index = this.getRandomInt(0, NUMBER_OF_ITEMS);
index = index >= NUMBER_OF_ITEMS ? 0 : index;
while (this.flag[index] === true) {
index = this.getRandomInt(0, NUMBER_OF_ITEMS);
index = index >= NUMBER_OF_ITEMS ? 0 : index;
}
this.flag[index] = true;
this.uiContext.animateTo({
duration: this.getRandomInt(MIN_ANM_DURATION, MAX_ANM_DURATION),
curve: Curve.Smooth,
onFinish: () => {
this.uiContext?.animateTo({
duration: this.getRandomInt(MIN_ANM_DURATION, MAX_ANM_DURATION),
curve: Curve.Smooth,
onFinish: () => {
this.uiContext?.animateTo({
duration: this.getRandomInt(MIN_ANM_DURATION, MAX_ANM_DURATION),
curve: Curve.Smooth,
onFinish: () => {
this.uiContext?.animateTo({
duration: this.getRandomInt(MIN_ANM_DURATION, MAX_ANM_DURATION),
curve: Curve.Smooth,
onFinish: () => {
}
}, () => {
this.scaleArray[index] = 0.6;
this.opacityArray[index] = 0.9;
this.offYArray[index] = -45;
this.offXArray[index] = this.getRandomInt(MIN_OFFSET_X, MAX_OFFSET_X);
})
}
}, () => {
this.scaleArray[index] = 0.5;
this.opacityArray[index] = 0.8;
this.offYArray[index] = -30;
this.offXArray[index] = this.getRandomInt(MIN_OFFSET_X, MAX_OFFSET_X);
})
}
}, () => {
this.scaleArray[index] = 0.3;
this.opacityArray[index] = 0.5;
this.offYArray[index] = -15;
this.offXArray[index] = this.getRandomInt(MIN_OFFSET_X, MAX_OFFSET_X);
})
}
}, () => {
this.scaleArray[index] = 0;
this.opacityArray[index] = 0;
this.offYArray[index] = 0;
this.offXArray[index] = 0;
})
}
}
可以发现,虽然二者可以实现相同的效果,但在代码的101行到110行实现了大量的嵌套操作,非常不利于代码的管理和解耦,并且会对代码的阅读效果产生影响。
【总结】
animateTo常用于实现单一属性从一个值到另一个值的平滑过渡,而keyframeAnimateTo关键帧动画主要用于实现多阶段、复杂路径的动画,支持多个关键帧和时间轴控制,二者虽可以实现相同的功能,但具体使用场景有显著区别,通常不可相互替代使用。
更多关于HarmonyOS鸿蒙Next中在animateTo的onFinish里嵌套animateTo是不是可以代替keyframeAnimateTo的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
感觉会不连贯
试了一下看不出区别啊,
可以打开开发者模式里的显示帧率再试试,如果没问题那就可以,
在HarmonyOS Next中,animateTo的onFinish回调内嵌套animateTo可以实现类似keyframeAnimateTo的序列动画效果。这种嵌套方式允许在前一个动画完成后触发下一个动画,形成连续动画序列。虽然语法和实现方式不同,但能达到类似的阶段性动画执行目的。不过需要注意动画执行时序和性能影响,嵌套层级过多可能导致动画延迟或卡顿。
在HarmonyOS Next中,使用animateTo的onFinish回调中嵌套另一个animateTo确实可以实现类似keyframeAnimateTo的连续动画效果,但两者存在关键差异:
优势:
- 灵活性更高,可在每个动画阶段动态调整参数(如时长、曲线)。
- 便于添加条件判断,实现非线性的动画序列。
缺点:
- 代码冗余:需手动管理多个动画块,
keyframeAnimateTo可通过数组集中定义关键帧,结构更清晰。 - 性能开销:嵌套回调可能增加微任务调度,影响流畅性(尤其在快速连续触发时)。
- 维护成本:多段动画的逻辑分散,调试复杂度更高。
- 时序精度:
onFinish依赖前一段动画结束,可能因系统负载产生微小延迟,而keyframeAnimateTo由系统统一调度时序。
适用场景:
- 需要动态切换动画参数的场景(如用户交互触发中断)。
- 简单两段式动画。
若需复杂多关键帧动画,优先推荐keyframeAnimateTo以保障性能与可读性。

