# 10.4:显式动画

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

# 10.4.1:显式动画定义介绍

declare function animateTo(value: AnimateParam, event: () => void): void;
1
  • 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%'})
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49

    运行结果如下图所示:

    10_4_1_1

# 10.3.2:自定义单步动画

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)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

样例运行结果如下图所示:

10_4_2_1

点击按钮后,执行全局 animateTo() 方法,animateTo() 方法的闭包函数内更改了 translate 的值,因此执行向下移动动画,如果向下移动动画结束后再执行向右移动动画,在向右移动动画结束后又执行向上移动动画等等,这就需要在上一个动画完整后再执行下一个动画……

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

Button('animateTo')
  .onClick(() => {
    animateTo({
      onFinish: () => {
        // 动画结束执行下一个动画
        animateTo({
          onFinish: () => {
            // 动画结束执行下一个动画
            // 省略....
          }
        }, () => {})
      }
    }, () => {
      // 省略
    })
  })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

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

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

Button('animateTo')
  .onClick(() => {
  this.animateStepX(() => {              // 调用animateStepX方法
    this.animateStepX(() => {            // 方法执行结束,调用下一步方法
      // 省略....
    })
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

根据伪代码看,虽然提取了 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);                // 参数非法,跳过执行
      }
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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);
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136

通过依次调用 step***() 方法,这样就构成了一个更为复杂连续的动画,样例运行结果如下所示:

10_4_2_2

# 10.3.2:小结

animateTo() 方法很好用,它为应用添加了更为丰富的动画体验,本章节实现的单步动画借助 Promise 实现,Promise 是一个比较重要的异步编程解决方案,读者需要掌握它的用法并理解它的工作原理,有不熟悉的读者请自行查阅文档,笔者就不介绍它了。

(adsbygoogle = window.adsbygoogle || []).push({});
请作者喝杯咖啡

津公网安备 12011402001367号

津ICP备2020008934号-2