1# 添加动画效果 2 3动画主要包含了组件动画和页面间动画,并提供了[插值计算](../reference/apis/js-apis-curve.md)和[矩阵变换](../reference/apis/js-apis-matrix4.md)的动画能力接口,让开发者极大程度的自主设计动画效果。 4 5在本节主要完成两个动画效果: 6 71. 启动页的闪屏动画,即Logo图标的渐出和放大效果; 82. 食物列表页和食物详情页的共享元素转场动画效果。 9 10## animateTo实现闪屏动画 11 12组件动画包括属性动画和animateTo显式动画: 13 141. 属性动画:设置组件通用属性变化的动画效果。 152. 显式动画:设置组件从状态A到状态B的变化动画效果,包括样式、位置信息和节点的增加删除等,开发者无需关注变化过程,只需指定起点和终点的状态。animateTo还提供播放状态的回调接口,是对属性动画的增强与封装。 16 17闪屏页面的动画效果是Logo图标的渐出和放大,动画结束后跳转到食物分类列表页面。接下来,我们就使用animateTo来实现启动页动画的闪屏效果。 18 191. 动画效果自动播放。闪屏动画的预期效果是,进入Logo页面后,animateTo动画效果自动开始播放,可以借助于组件显隐事件的回调接口来实现。调用Shape的onAppear方法,设置其显式动画。 20 21 ```ts 22 Shape() { 23 ... 24 } 25 .onAppear(() => { 26 animateTo() 27 }) 28 ``` 29 302. 创建opacity和scale数值的成员变量,用装饰器@State修饰。表示其为有状态的数据,即改变会触发页面的刷新。 31 32 ```ts 33 @Entry 34 @Component 35 struct Logo { 36 @State private opacityValue: number = 0 37 @State private scaleValue: number = 0 38 build() { 39 Shape() { 40 ... 41 } 42 .scale({ x: this.scaleValue, y: this.scaleValue }) 43 .opacity(this.opacityValue) 44 .onAppear(() => { 45 animateTo() 46 }) 47 } 48 } 49 ``` 50 513. 设置animateTo的动画曲线curve。Logo的加速曲线为先慢后快,使用贝塞尔曲线cubicBezier,cubicBezier(0.4, 0, 1, 1)。 52 53 需要使用动画能力接口中的插值计算,首先要导入curves模块。 54 55 ```ts 56 import Curves from '@ohos.curves' 57 ``` 58 59 @ohos.curves模块提供了线性Curve. Linear、阶梯step、三阶贝塞尔(cubicBezier)和弹簧(spring)插值曲线的初始化函数,可以根据入参创建一个插值曲线对象。 60 61 ```ts 62 @Entry 63 @Component 64 struct Logo { 65 @State private opacityValue: number = 0 66 @State private scaleValue: number = 0 67 private curve1 = Curves.cubicBezier(0.4, 0, 1, 1) 68 69 build() { 70 Shape() { 71 ... 72 } 73 .scale({ x: this.scaleValue, y: this.scaleValue }) 74 .opacity(this.opacityValue) 75 .onAppear(() => { 76 animateTo({ 77 curve: this.curve1 78 }) 79 }) 80 } 81 } 82 ``` 83 844. 设置动画时长为1s,延时0.1s开始播放,设置显示动效event的闭包函数,即起点状态到终点状态为透明度opacityValue和大小scaleValue从0到1,实现Logo的渐出和放大效果。 85 86 ```ts 87 @Entry 88 @Component 89 struct Logo { 90 @State private opacityValue: number = 0 91 @State private scaleValue: number = 0 92 private curve1 = Curves.cubicBezier(0.4, 0, 1, 1) 93 94 build() { 95 Shape() { 96 ... 97 } 98 .scale({ x: this.scaleValue, y: this.scaleValue }) 99 .opacity(this.opacityValue) 100 .onAppear(() => { 101 animateTo({ 102 duration: 1000, 103 curve: this.curve1, 104 delay: 100, 105 }, () => { 106 this.opacityValue = 1 107 this.scaleValue = 1 108 }) 109 }) 110 } 111 } 112 ``` 113 1145. 闪屏动画播放结束后定格1s,进入FoodCategoryList页面。设置animateTo的onFinish回调接口,调用定时器Timer的setTimeout接口延时1s后,调用router.replace,显示FoodCategoryList页面。 115 116 ```ts 117 import router from '@ohos.router' 118 119 @Entry 120 @Component 121 struct Logo { 122 @State private opacityValue: number = 0 123 @State private scaleValue: number = 0 124 private curve1 = Curves.cubicBezier(0.4, 0, 1, 1) 125 126 build() { 127 Shape() { 128 ... 129 } 130 .scale({ x: this.scaleValue, y: this.scaleValue }) 131 .opacity(this.opacityValue) 132 .onAppear(() => { 133 134 animateTo({ 135 duration: 1000, 136 curve: this.curve1, 137 delay: 100, 138 onFinish: () => { 139 setTimeout(() => { 140 router.replaceUrl({ url: "pages/FoodCategoryList" }) 141 }, 1000); 142 } 143 }, () => { 144 this.opacityValue = 1 145 this.scaleValue = 1 146 }) 147 }) 148 } 149 } 150 ``` 151 152 整体代码如下。 153 154 ```ts 155 import Curves from '@ohos.curves' 156 import router from '@ohos.router' 157 158 @Entry 159 @Component 160 struct Logo { 161 @State private opacityValue: number = 0 162 @State private scaleValue: number = 0 163 private curve1 = Curves.cubicBezier(0.4, 0, 1, 1) 164 private pathCommands1: string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z' 165 private pathCommands2: string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z' 166 167 build() { 168 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 169 Shape() { 170 Path() 171 .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36') 172 .fill(Color.White) 173 .stroke(Color.Transparent) 174 Path() 175 .commands(this.pathCommands1) 176 .fill('none') 177 .stroke(Color.Transparent) 178 .linearGradient( 179 { 180 angle: 30, 181 colors: [["#C4FFA0", 0], ["#ffffff", 1]] 182 }) 183 .clip(new Path().commands(this.pathCommands1)) 184 185 Path() 186 .commands(this.pathCommands2) 187 .fill('none') 188 .stroke(Color.Transparent) 189 .linearGradient( 190 { 191 angle: 50, 192 colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]] 193 }) 194 .clip(new Path().commands(this.pathCommands2)) 195 } 196 .height('630px') 197 .width('630px') 198 .scale({ x: this.scaleValue, y: this.scaleValue }) 199 .opacity(this.opacityValue) 200 .onAppear(() => { 201 animateTo({ 202 duration: 1000, 203 curve: this.curve1, 204 delay: 100, 205 onFinish: () => { 206 setTimeout(() => { 207 router.replaceUrl({ url: "pages/FoodCategoryList" }) 208 }, 1000); 209 } 210 }, () => { 211 this.opacityValue = 1 212 this.scaleValue = 1 213 }) 214 }) 215 216 Text('Healthy Diet') 217 .fontSize(26) 218 .fontColor(Color.White) 219 .margin({ top: 300 }) 220 221 Text('Healthy life comes from a balanced diet') 222 .fontSize(17) 223 .fontColor(Color.White) 224 .margin({ top: 4 }) 225 } 226 .width('100%') 227 .height('100%') 228 .linearGradient( 229 { 230 angle: 180, 231 colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]] 232 }) 233 } 234 } 235 ``` 236 237  238 239## 页面转场动画 240 241食物分类列表页和食物详情页之间的共享元素转场,即点击FoodListItem/FoodGridItem后,食物缩略图会放大,随着页面跳转,到食物详情页的大图。 242 2431. 设置FoodListItem和FoodGridItem的Image组件的共享元素转场方法(sharedTransition)。转场id为foodItem.id,转场动画时长为1s,延时0.1s播放,变化曲线为贝塞尔曲线Curves.cubicBezier(0.2, 0.2, 0.1, 1.0) ,需引入curves模块。 244 245 共享转场时会携带当前元素的被设置的属性,所以创建Row组件,使其作为Image的父组件,设置背景颜色在Row上。 246 247 在FoodListItem的Image组件上设置autoResize为false,因为image组件默认会根据最终展示的区域,去调整图源的大小,以优化图片渲染性能。在转场动画中,图片在放大的过程中会被重新加载,所以为了转场动画的流畅,autoResize设置为false。 248 249 ```ts 250 // FoodList.ets 251 import Curves from '@ohos.curves' 252 253 @Component 254 struct FoodListItem { 255 private foodItem: FoodData 256 build() { 257 Navigator({ target: 'pages/FoodDetail' }) { 258 Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { 259 Row() { 260 Image(this.foodItem.image) 261 .objectFit(ImageFit.Contain) 262 .autoResize(false) 263 .height(40) 264 .width(40) 265 .sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 }) 266 } 267 268 .margin({ right: 16 }) 269 Text(this.foodItem.name) 270 .fontSize(14) 271 .flexGrow(1) 272 Text(this.foodItem.calories + ' kcal') 273 .fontSize(14) 274 } 275 .height(64) 276 } 277 .params({ foodData: this.foodItem }) 278 .margin({ right: 24, left:32 }) 279 } 280 } 281 282 @Component 283 struct FoodGridItem { 284 private foodItem: FoodData 285 build() { 286 Column() { 287 Row() { 288 Image(this.foodItem.image) 289 .objectFit(ImageFit.Contain) 290 .autoResize(false) 291 .height(152) 292 .width('100%') 293 .sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 }) 294 } 295 Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { 296 Text(this.foodItem.name) 297 .fontSize(14) 298 .flexGrow(1) 299 .padding({ left: 8 }) 300 Text(this.foodItem.calories + 'kcal') 301 .fontSize(14) 302 .margin({ right: 6 }) 303 } 304 .height(32) 305 .width('100%') 306 .backgroundColor('#FFe5e5e5') 307 } 308 .height(184) 309 .width('100%') 310 .onClick(() => { 311 router.pushUrl({ url: 'pages/FoodDetail', params: { foodData: this.foodItem } }) 312 }) 313 } 314 } 315 316 317 ``` 318 3192. 设置FoodDetail页面的FoodImageDisplay的Image组件的共享元素转场方法(sharedTransition)。设置方法同上。 320 321 ```ts 322 import Curves from '@ohos.curves' 323 324 @Component 325 struct FoodImageDisplay { 326 private foodItem: FoodData 327 build() { 328 Stack({ alignContent: Alignment.BottomStart }) { 329 Image(this.foodItem.image) 330 .objectFit(ImageFit.Contain) 331 .sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 }) 332 Text(this.foodItem.name) 333 .fontSize(26) 334 .fontWeight(500) 335 .margin({ left: 26, bottom: 17.4 }) 336 } 337 .height(357) 338 } 339 } 340 ``` 341 342  343 344 通过对绘制组件和动画的学习,我们已完成了启动Logo的绘制、启动页动画和页面间的转场动画,声明式UI框架提供了丰富的动效接口,合理地应用和组合可以让应用更具有设计感。 345 346 347