1# 如何实现逐帧动画
2
3## 场景说明
4逐帧动画是常见的一种动画呈现形式,本例就为大家介绍如何通过translate(),setInterval(),clearAllInterval()等方法实现逐帧动画。
5
6## 效果呈现
7本例最终效果如下:
8- 点击“run”按钮,火柴人开始走动。
9- 点击“stop”按钮,火柴人停止走动。
10
11![frameanimation](figures/frameanimation.gif)
12
13## 运行环境
14- IDE:DevEco Studio 3.1 Beta1
15- SDK:Ohos_sdk_public 3.2.11.9 (API Version 9 Release)
16
17## 实现思路
18本例的实现有两个关键点:
19
20- 将连续走动的火柴人拆分为多帧静态图像,在固定的时间间隔内逐帧将图像移动到动画窗口,间隔时间要小于肉眼可察觉的时间。循环上述动作,就可以实现火柴人的走动动画。
21火柴人静态图像如下:
22
23  ![man](figures/man.png)
24
25- 将背景图片以固定速度相对于火柴人走动方向反方向移动,从而实现火柴人向前走动的效果。
26背景图如下:
27![background](figures/background.png)
28
29本例使用translate()控制火柴人的移动,用backgroundImagePosition()控制背景图的移动。另外,通过setInterval()设置火柴人移动的时间间隔,通过clearAllInterval()清除移动。
30
31## 开发步骤
321. 搭建UI框架。
33
34   使用两个Row组件分别呈现背景图和火柴人,第二个Row组件作为第一个Row组件的子组件,父Row组件的背景设置为背景图,子Row组件中添加Image组件用来呈现火柴人单帧图像。
35    ```ts
36    @Entry
37    @Component
38    export default struct frameAnimation {
39      build() {
40        Column() {
41          // 父Row组件
42          Row() {
43            // 子Row组件
44            Row() {
45              // 通过Image组件显示火柴人图像
46              Image($r("app.media.man")).height(60).width(545.16)
47            }.width(100)
48            .justifyContent(FlexAlign.Start)
49            .alignItems(VerticalAlign.Top)
50            // 截取显示与背景同等大小的区域,控制单个火柴人显示在画面中
51            .clip(true)
52          }
53          // 添加背景图像
54          .backgroundImage($r("app.media.background"))
55          // 保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界。
56          .backgroundImageSize(ImageSize.Cover)
57          .width('100%')
58          .height(130)
59          .justifyContent(FlexAlign.Center)
60          .alignItems(VerticalAlign.Bottom)
61          Row() {
62            // 添加跑动按钮
63            Button('run')
64              .margin({ right: 10 })
65              .type(ButtonType.Normal)
66              .width(75)
67              .borderRadius(5)
68            // 添加停止按钮
69            Button('stop')
70              .type(ButtonType.Normal)
71              .borderRadius(5)
72              .width(75)
73              .backgroundColor('#ff0000')
74          }.margin({ top: 30, bottom: 10 })
75        }.width('100%').width('100%').padding({ top: 30 })
76      }
77    }
78    ```
792. 添加火柴人和背景图片的移动逻辑。
80
81   通过状态变量设定火柴人和背景图片的位置,位置变化时可以实时刷新UI界面。
82    ```ts
83    // 火柴人位置变量
84    @State manPostion: {
85      x: number,
86      y: number
87    } = { x: 0, y: 0 }
88    // 背景图位置变量
89    @State treePosition: {
90      x: number,
91      y: number
92    } = { x: 0, y: 0 }
93    ```
94   给火柴人和背景图片添加位置属性。
95    ```ts
96    Row() {
97      Row() {
98        Image($r("app.media.man"))
99          .height(60)
100          .width(545.16)
101          // 通过translate实现火柴人的位移。绑定manPosition,用来改变火柴人位置。
102          .translate(this.manPostion)
103      }
104      ...
105    }
106    .backgroundImage($r("app.media.background"))
107    .backgroundImageSize(ImageSize.Cover)
108    // 通过backgroundImagePosition实现背景图片的位移。绑定treePosition,用来改变背景图片的位置。
109    .backgroundImagePosition(this.treePosition)
110    ...
111    ```
1123. 为''run''按钮和"stop"按钮绑定控制逻辑。
113
114   构建火柴人和背景图片移动的方法,用来设定火柴人和背景图片每次移动的距离。这里要注意火柴人每次移动的距离等于两个火柴人之间的间隔距离(像素值)。
115    ```ts
116    // 火柴人移动方法
117    manWalk() {
118      if (this.manPostion.x <= -517.902) {
119        this.manPostion.x = 0
120      } else {
121        // 每次移动的距离为火柴人静态图像之间的间隔距离
122        this.manPostion.x -= 129.69
123      }
124    }
125    // 背景移动方法
126    treesMove() {
127      if (this.treePosition.x <= -1215) {
128        this.treePosition.x = 0
129      } else {
130        this.treePosition.x -= 20
131      }
132    }
133    ```
134   创建doAnimation()方法调用上述两个方法,以便在后续的定时器中使用。
135    ```ts
136    doAnimation() {
137      this.manWalk()
138      this.treesMove()
139      }
140    ```
141   通过setInterval为“run”按钮绑定走动逻辑。
142    ```ts
143    Button('run')
144      .margin({ right: 10 })
145      .type(ButtonType.Normal)
146      .width(75)
147      .borderRadius(5)
148      .onClick(() => {
149        this.clearAllInterval()
150        // 创建定时器,调用doAnimation方法,启动动画
151        let timer = setInterval(this.doAnimation.bind(this), 100)
152        this.timerList.push(timer)
153      })
154    ```
155   通过clearAllInterval为“stop”按钮绑定停止逻辑。
156    ```ts
157    Button('stop')
158      .type(ButtonType.Normal)
159      .borderRadius(5)
160      .width(75)
161      .backgroundColor('#ff0000')
162      .onClick(() => {
163        // 清理定时器,停止动画
164        this.clearAllInterval()
165    })
166    ```
167
168## 完整代码
169本例完整代码如下:
170```ts
171@Entry
172@Component
173export default struct frameAnimation {
174  // 火柴人位置变量
175  @State manPostion: {
176    x: number,
177    y: number
178  } = { x: 0, y: 0 }
179  // 背景图位置变量
180  @State treePosition: {
181    x: number,
182    y: number
183  } = { x: 0, y: 0 }
184  // 定时器列表,当列表清空时,动画停止
185  private timerList: number[] = []
186
187  // 火柴人移动方法
188  manWalk() {
189    if (this.manPostion.x <= -517.902) {
190      this.manPostion.x = 0
191    } else {
192      this.manPostion.x -= 129.69
193    }
194  }
195  // 背景移动方法
196  treesMove() {
197    if (this.treePosition.x <= -1215) {
198      this.treePosition.x = 0
199    } else {
200      this.treePosition.x -= 20
201    }
202  }
203
204  // 销毁所有定时器
205  clearAllInterval() {
206    this.timerList.forEach((timer: number) => {
207      clearInterval(timer)
208    })
209    this.timerList = []
210  }
211
212  doAnimation() {
213    this.manWalk()
214    this.treesMove()
215  }
216
217  build() {
218    Column() {
219      // 父Row组件
220      Row() {
221        // 子Row组件
222        Row() {
223          // 通过Image组件显示火柴人图像
224          Image($r("app.media.man"))
225            .height(60)
226            .width(545.16)
227            // 通过translate实现火柴人的位移。绑定manPosition变量,用来改变火柴人位置。
228            .translate(this.manPostion)
229        }
230        .width(100)
231        .justifyContent(FlexAlign.Start)
232        .alignItems(VerticalAlign.Top)
233        // 截取显示与背景同等大小的区域,控制单个火柴人显示在画面中
234        .clip(true)
235      }
236      // 添加背景图像
237      .backgroundImage($r("app.media.background"))
238      // 保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界。
239      .backgroundImageSize(ImageSize.Cover)
240      // 通过backgroundImagePosition实现背景图片的位移。绑定treePosition,用来改变背景图片的位置。
241      .backgroundImagePosition(this.treePosition)
242      .width('100%')
243      .height(130)
244      .justifyContent(FlexAlign.Center)
245      .alignItems(VerticalAlign.Bottom)
246
247      Row() {
248        // 添加跑动按钮
249        Button('run')
250          .margin({ right: 10 })
251          .type(ButtonType.Normal)
252          .width(75)
253          .borderRadius(5)
254          .onClick(() => {
255            this.clearAllInterval()
256            let timer = setInterval(this.doAnimation.bind(this), 100)
257            this.timerList.push(timer)
258          })
259        // 添加停止按钮
260        Button('stop')
261          .type(ButtonType.Normal)
262          .borderRadius(5)
263          .width(75)
264          .backgroundColor('#ff0000')
265          .onClick(() => {
266            this.clearAllInterval()
267          })
268      }.margin({ top: 30, bottom: 10 })
269    }.width('100%').width('100%').padding({ top: 30 })
270  }
271}
272```
273
274## 参考
275- [Timer (定时器)](../application-dev/reference/common/js-apis-timer.md)
276- [图形变换](../application-dev/reference/apis-arkui/arkui-ts/ts-universal-attributes-transformation.md)
277- [背景设置](../application-dev/reference/apis-arkui/arkui-ts/ts-universal-attributes-background.md)
278
279