Skip to content

显式动画

显示动画由全局方法 animateTo 实现,它主要是解决由于闭包代码导致的状态变化而插入的动画效果。

显式动画定义介绍

bash

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:

指定显示动效的闭包函数,在闭包函数中导致的状态变化系统会自动插入过渡动画。

简单样例如下所示:

bash

@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() 对控件做单个位移动画,演示样例如下所示:

bash

@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 的值,因此执行向下移动动画,如果向下移动动画结束后再执行向右移动动画,在向右移动动画结束后又执行向上移动动画等等,这就需要在上一个动画完整后再执行下一个动画……

针对这种场景,使用传统的回调嵌套可以实现,伪代码如下:

bash

Button('animateTo')
  .onClick(() => {
    animateTo({
      onFinish: () => {
        // 动画结束执行下一个动画
        animateTo({
          onFinish: () => {
            // 动画结束执行下一个动画
            // 省略....
          }
        }, () => {})
      }
    }, () => {
      // 省略
    })
  })

这种多层次次嵌套代码是及其不合理的,可以稍微做优化,把单个动画执行提取出一个方法,伪代码如下:

bash

private animateStepX(event: () => void) {// 封装animateStepX方法
  animateTo({
    onFinish: () => {
      event?.call(this);
    }
  }, () => {});
}

Button('animateTo')
  .onClick(() => {
  this.animateStepX(() => {              // 调用animateStepX方法
    this.animateStepX(() => {            // 方法执行结束,调用下一步方法
      // 省略....
    })
  })
})

根据伪代码看,虽然提取了 animateStepX() 方法出来,但是这种方式依然没有解决多层次嵌套的问题,对于这种场景,Promise 的实用性就体现出来了,它可以有效解决层级嵌套的问题,对 Promise 不熟悉的读者请自行了解一下。

利用 Promise 特性,封装一个单步动画方法,待每一步动画执行结束后再执行下一个动画,单步动画方法封装如下:

bash

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> ,这样就可以实现单个方法的执行控制了,使用样例如下所示:

bash

@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***() 方法,这样就构成了一个更为复杂连续的动画,样例运行结果如下所示:

图片