HarmonyOS鸿蒙Next中在animateTo的onFinish里嵌套animateTo是不是可以代替keyframeAnimateTo

HarmonyOS鸿蒙Next中在animateTo的onFinish里嵌套animateTo是不是可以代替keyframeAnimateTo 感觉这样用功能比keyframeAnimateTo更自由,有什么坏处吗?

6 回复

【背景知识】

  • UIContext提供animateTo接口来指定由于闭包代码导致的状态变化插入过渡动效。
  • 在UIContext中同样提供keyframeAnimateTo接口来指定若干个关键帧状态,实现分段的动画。同属性动画,布局类改变宽高的动画,内容都是直接到终点状态,例如文字、Canvas的内容等,如果要内容跟随宽高变化,可以使用renderFit属性配置。

【解决方案】

  1. 以下是一个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);
        }
      },
    ])
  }
}
  1. 若想通过在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中,使用animateToonFinish回调中嵌套另一个animateTo确实可以实现类似keyframeAnimateTo的连续动画效果,但两者存在关键差异:

优势

  • 灵活性更高,可在每个动画阶段动态调整参数(如时长、曲线)。
  • 便于添加条件判断,实现非线性的动画序列。

缺点

  1. 代码冗余:需手动管理多个动画块,keyframeAnimateTo可通过数组集中定义关键帧,结构更清晰。
  2. 性能开销:嵌套回调可能增加微任务调度,影响流畅性(尤其在快速连续触发时)。
  3. 维护成本:多段动画的逻辑分散,调试复杂度更高。
  4. 时序精度onFinish依赖前一段动画结束,可能因系统负载产生微小延迟,而keyframeAnimateTo由系统统一调度时序。

适用场景

  • 需要动态切换动画参数的场景(如用户交互触发中断)。
  • 简单两段式动画。
    若需复杂多关键帧动画,优先推荐keyframeAnimateTo以保障性能与可读性。
回到顶部