1# 如何实现逐帧动画 2 3## 场景说明 4逐帧动画是常见的一种动画呈现形式,本例就为大家介绍如何通过translate(),setInterval(),clearAllInterval()等方法实现逐帧动画。 5 6## 效果呈现 7本例最终效果如下: 8- 点击“run”按钮,火柴人开始走动。 9- 点击“stop”按钮,火柴人停止走动。 10 11 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  24 25- 将背景图片以固定速度相对于火柴人走动方向反方向移动,从而实现火柴人向前走动的效果。 26背景图如下: 27 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