1# 属性动画 2 3## 场景介绍 4在日常开发过程中,通常会出现因为状态变化导致组件属性值发生变化,如: 5- 数字键盘按键时,对应数字按钮背景色加深,呈现被点击效果;放开按键时,呈现取消选中效果, 6- UI中图标按下时,图标出现弹性缩放效果, 7- 在做数据统计时,随着数据值的变化,统计曲线随之变化等动画效果, 8本例将为大家介绍下如何通过属性动画实现上述场景。 9 10## 效果呈现 11效果图如下: 12 13| 场景 | 效果图 | 14|------------|-----------------------| 15| 场景1:属性动画 |  | 16| 场景2:弹性动态 |  | 17| 场景3:曲线动画 |  | 18 19## 运行环境 20本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发 21- IDE: DevEco Studio 3.1 Release 22- SDK: Ohos_sdk_public 3.2.12.5(API Version 9 Release) 23 24## 场景1:属性动画 25### 实现思路 26本实例涉及到的主要特性及其实现方案如下: 27* 通过Column、Grid、button、Text等关键组件以及visibility属性搭建UI局框架,以及渲染数字按钮。 28* 设置状态变量flag,用来控制属性状态的变化,同时向Button组件添加onTouch事件,更新按钮的当前状态,从而可以根据监听到的按钮状态加载对应的动画效果。 29 * 默认状态为按钮放开状态(flag为false)。 30 * 当按钮按下时,更新按钮的状态(flag:false -> true)。 31 * 当按钮放开时,更新按钮的状态(flag:true -> false)。 32* 根据按钮组件的按下/放开状态,通过三元运算判断,使用属性动画更新按钮的选中/取消效果。 33 * 当按钮按下时,加载属性动画:按钮被选中效果。 34 * 当按钮放开时,加载属性动画:按钮取消选中效果。 35### 开发步骤 36针对实现思路中所提到的内容,具体关键开发步骤如下: 371. 通过Column、Grid、button、Text等关键组件以及visibility属性搭建UI框架,以及渲染数字按钮。 38 具体代码如下: 39 40 ```ts 41 42 private numGrid: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, -1, 0, -1] 43 44 build() { 45 Row() { 46 Column() { 47 Grid() { 48 //通过ForEach循环遍历,显示数字数字按钮 49 ForEach(this.numGrid, (item, index) => { 50 GridItem() { 51 Button() { 52 Text(`${item}`) 53 .fontSize(30) 54 } 55 .backgroundColor('#fff') 56 .width(100) 57 .height(100) 58 // item为-1时,数字按钮不显示 59 .visibility(item == -1 ? Visibility.Hidden : Visibility.Visible) 60 } 61 }, item => item) 62 } 63 .columnsTemplate('1fr 1fr 1fr') 64 .rowsTemplate('1fr 1fr 1fr 1fr') 65 .columnsGap(10) 66 .rowsGap(10) 67 .width(330) 68 .height(440) 69 } 70 .width('100%') 71 .height('100%') 72 } 73 } 74 75 ``` 76 772. 设置状态变量flag,用来控制属性状态的变化,同时向Button组件添加onTouch事件,更新按钮的当前状态,从而可以根据监听到的按钮状态加载对应的动画效果。 78 79 具体代码如下: 80 81 ```ts 82 //状态变量flag,用于监听按钮按下和放开的状态,默认至为false(放开状态) 83 @State flag: boolean = false; 84 ... 85 .onTouch((event:TouchEvent) => { 86 //当按钮按下时,更新按钮的状态(flag:false -> true) 87 if (event.type == TouchType.Down) { 88 this.flag = true 89 this.currIndex = index 90 } 91 //当按钮放开时,更新按钮的状态(flag:true -> false) 92 if (event.type == TouchType.Up) { 93 this.flag = false 94 } 95 }) 96 ``` 97 98 993. 根据按钮组件的按下/放开状态,通过三元运算判断并更新按钮背景色。 100 101 具体代码如下: 102 103 ```ts 104 Button(){ 105 ... 106 // 当按钮被按下(flag变更为true)时,当前按钮backgroundColor属性变更("#fff" -> "#D2C3D5" ) 107 // 当按钮被放开(flag变更为false)时,当前按钮backgroundColor属性变更("#D2C3D5" -> "#FFF" ) 108 .backgroundColor(this.flag && this.currIndex == index ? '#D2C3D5' : '#fff') 109 .animation({ 110 duration: 200 111 }) 112 } 113 ``` 114 115 116### 完整代码 117```ts 118@Entry 119@Component 120struct Index { 121 private currIndex: number = -1 122 private numGrid: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, -1, 0, -1] 123 //状态变量flag,用于监听按钮按下和放开的状态,默认至为false(放开状态) 124 @State flag: boolean = false; 125 126 build() { 127 Row() { 128 Column() { 129 Grid() { 130 ForEach(this.numGrid, (item, index) => { 131 GridItem() { 132 Button() { 133 Text(`${item}`) 134 .fontSize(30) 135 } 136 // 当按钮被按下(flag变更为true)时,当前按钮backgroundColor属性变更("#fff" -> "#D2C3D5" ) 137 // 当按钮被放开(flag变更为false)时,当前按钮backgroundColor属性变更("#D2C3D5" -> "#FFF" ) 138 .backgroundColor(this.flag && this.currIndex == index ? '#D2C3D5' : '#fff') 139 .animation({ 140 duration: 200 141 }) 142 .width(100) 143 .height(100) 144 .visibility(item == -1 ? Visibility.Hidden : Visibility.Visible) 145 .onTouch((event:TouchEvent) => { 146 //当按钮按下时,更新按钮的状态(flag:false -> true) 147 if (event.type == TouchType.Down) { 148 this.flag = true 149 this.currIndex = index 150 } 151 //当按钮放开时,更新按钮的状态(flag:true -> false) 152 if (event.type == TouchType.Up) { 153 this.flag = false 154 } 155 }) 156 } 157 }, item => item) 158 } 159 .columnsTemplate('1fr 1fr 1fr') 160 .rowsTemplate('1fr 1fr 1fr 1fr') 161 .columnsGap(10) 162 .rowsGap(10) 163 .width(330) 164 .height(440) 165 } 166 .width('100%') 167 .height('100%') 168 } 169 } 170} 171``` 172 173## 场景2:弹性动效 174### 实现思路 175针对弹性动效,涉及到的主要特征分为两部分: 176* 组件描绘:先通过List、ListItem、Image等组件将控件表描绘出来。 177* 组件状态变化:设置状态变量downFlag,控制按钮的当前状态;同时向Column组件添加onTouch事件,监听组件的当前状态。 178 * 组件默认状态为放开状态(downFlag:false)。 179 * 当按下Image组件时,更新组件的状态为true。 180 * 当放开Image组件时,更新组件的状态为false。 181* 动画播放:使用属性动画绘制Image组件不同状态下的曲线动画。 182 * 组件按下,触发第一阶段缩小动效,370ms内将组件横纵尺寸均缩小到原尺寸的80%。 183 * 组件放开,触发第二阶段放大动效,在370ms内将控件从当前尺寸恢复到原尺寸。 184### 开发步骤 185针对实现思路中所提到的内容,具体关键开发步骤如下: 1861. 先通过List、ListItem、Image等组件将控件表描绘出来。 187 188 具体代码如下: 189 190 ```ts 191 private arr: number[]=[1,2,3,4,5] 192 193 ... 194 List({space:10}){ 195 ForEach(this.arr,(item,index)=>{ 196 ListItem(){ 197 Image($r("app.media.app_icon")) 198 .width(80) 199 .height(80) 200 } 201 }) 202 } 203 .margin({top:20}) 204 .padding({left:20}) 205 .listDirection(Axis.Horizontal) 206 ``` 207 2082. 设置状态变量downFlag,控制Image组件的当前状态,同时向Image组件添加onTouch事件,获取并更新组件的当前状态,从而可以根据监听到的组件状态加载对应的动画效果。 209 具体代码如下: 210 211 ```ts 212 ... 213 // 状态变量downFlag,用于监听Image组件按下和放开的状态 214 @State downFlag: boolean = false; 215 ... 216 // 添加onTouch事件,监听状态 217 .onTouch((event: TouchEvent) => { 218 // 当Column组件按下时,组件的状态更新为true 219 if (event.type == TouchType.Down) { 220 this.downFlag = true 221 // 当Column组件按下时,组件的状态更新为false 222 } else if (event.type == TouchType.Up) { 223 this.downFlag = false 224 } 225 }) 226 ``` 227 2283. 根据Image组件的按下/放开状态,呈现不同的弹性效果(按下时组件缩小,动画以阻尼曲线的形式缩小至0.8倍,放开时组件动画以阻尼曲线的形式恢复至初始大小)。 229 具体代码如下: 230 231 ```ts 232 ··· 233 Image($r("app.media.app_icon")) 234 // 当downFlag状态为按下(true)时,组件在370ms内缩小至0.5倍;当downFlag状态为放开(false)时,组件在370ms内恢复至初始大小; 235 .scale(this.downFlag ? { x: 0.8, y: 0.8 } : { x: 1, y: 1 }) 236 .animation({ 237 duration: 370, 238 curve: Curve.Friction 239 }) 240 ``` 241 242### 完整代码 243```ts 244@Entry 245@Component 246struct Index { 247 // 状态变量downFlag,用于监听Image组件按下和放开的状态 248 @State downFlag: boolean = false; 249 private arr: number[]=[1,2,3,4,5] 250 private curIndex : number = -1 251 252 build() { 253 List({space:10}){ 254 ForEach(this.arr,(item,index)=>{ 255 ListItem(){ 256 Image($r("app.media.app_icon")) 257 // 当downFlag状态为按下(true)时,组件在370ms内缩小至0.8倍;当downFlag状态为放开(false)时,组件在370ms内恢复至初始大小; 258 .scale(this.downFlag && this.curIndex == index ? { x: 0.8, y: 0.8 } : { x: 1, y: 1 }) 259 .animation({ 260 duration: 370, 261 curve: Curve.Friction 262 }) 263 .width(80) 264 .height(80) 265 // 添加onTouch事件,监听状态 266 .onTouch((event: TouchEvent) => { 267 // 当Image组件按下时,组件的状态更新为true 268 if (event.type == TouchType.Down) { 269 this.downFlag = true 270 this.curIndex = index 271 // 当Image组件按下时,组件的状态更新为false 272 } else if (event.type == TouchType.Up) { 273 this.downFlag = false 274 } 275 }) 276 } 277 }) 278 } 279 .margin({top:20}) 280 .padding({left:20}) 281 // 列表排列方向水平 282 .listDirection(Axis.Horizontal) 283 } 284} 285``` 286 287## 场景3:曲线动画 288曲线动画可以使用不同的曲线属性,呈现出不同的动画。 289ArkUI中,曲线动画有两种使用方式,一种是直接使用Curve类型的枚举,一种是导入ohos.curve模块并使用模块内定义的接口/属性。 290 291### 实现思路 292* 设置自定义变量item,用于存放不同类型的曲线,本案例中主要有以下类型曲线: 293 1. Curve类型的枚举: 294 * Linear:表示动画从头到尾的速度都是相同的。 295 * EaseInOut:表示动画以低速开始和结束,CubicBezier(0.42, 0.0, 0.58, 1.0)。 296 * Smooth:平滑曲线,cubic-bezier(0.4, 0.0, 0.4, 1.0)。 297 2. ohos.curve导入: 298 * springCurve类的弹簧曲线。 299* 通过Flex、Column、button组件将UI布局以及框架搭建。 300* Column组件内部通过ForEach,遍历展示每条曲线。 301* 设置状态变量flag,监听当前button的状态,同时向button组件添加onClick事件,更新按钮的状态,从而可以根据监听到的按钮状态加载对应的动画效果。 302* 向Text组件添加width属性,根据按钮的状态,在同一周期内,通过width的变化呈现曲线的不同动画。 303### 开发步骤 3041. 设置自定义变量item,用于存放不同类型的曲线。 305 306 具体代码如下: 307 308 ```ts 309 private items: object[] = [ 310 { 311 color: '#a320bf', 312 title: '线性曲线', 313 // curve类型曲线:表示动画从头到尾的速度都是相同的。 314 curve: Curve.Linear, 315 }, 316 { 317 color: '#17d4be', 318 title: '低速结束曲线', 319 // curve类型曲线:表示动画以低速结束 320 curve: Curve.EaseInOut, 321 }, 322 { 323 color: '#0e2d8c', 324 title: '平滑曲线', 325 // curve类型曲线:平滑曲线 326 curve: Curve.Smooth, 327 }, 328 { 329 color: '#d4bb19', 330 title: 'Curves.spring', 331 // ohos.curve导入的弹簧曲线,以初速度为20的弹簧进行平移 332 curve: Curves.springCurve(20, 1, 1, 1.2), 333 }, 334 ``` 335 3362. 通过Flex、Column、button组件将UI布局以及框架搭建。 337 338 具体代码如下: 339 340 ```ts 341 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { 342 Column({ space: 50 }) { 343 ... 344 } 345 .alignItems(HorizontalAlign.Start) 346 .width('100%') 347 .padding({ left: 12, right: 12, top: 22, bottom: 22 }) 348 349 Button(){ 350 Text("Go!") 351 .fontSize(20) 352 .fontColor('#fff') 353 } 354 .borderRadius(20) 355 .fontColor('#FFFFFF') 356 .fontSize('12fp') 357 .fontWeight(FontWeight.Bolder) 358 .backgroundColor('#15587c') 359 .padding(25) 360 .margin({top:50}) 361 } 362 .height('100%') 363 .backgroundColor('#F1F3F5') 364 .padding({ left: '3%', right: '3%' }) 365 ``` 366 3673. Column组件内部通过ForEach,遍历展示每条曲线。 368 369 具体代码如下: 370 371 ```ts 372 ForEach(this.items, (item, index) => { 373 Text(item['title']) 374 .fontSize(20) 375 .fontColor('#FFFFFF') 376 .height(80) 377 .textAlign(TextAlign.Start) 378 .backgroundColor(item['color']) 379 .borderRadius(20) 380 .padding({left:10}) 381 }, item => JSON.stringify(item)) 382 383 ``` 384 3854. 设置状态变量flag,监听当前button的状态,同时向button组件添加onClick事件,更新按钮的状态,从而可以根据监听到的按钮状态加载对应的动画效果。 386 387 具体代码如下: 388 389 ```ts 390 //状态变量flag,用于监听按钮按下状态 391 @State flag: boolean = true 392 ... 393 Button(){ 394 } 395 // 通过点击事件,反转flag的值 396 .onClick(() => { 397 this.flag = !this.flag 398 }) 399 ``` 400 4015. 向Text组件添加width属性,根据按钮的状态,在同一周期内,呈现不同效果的曲线动画。 402 403 具体代码如下: 404 405 ```ts 406 ForEach(this.items, (item, index) => { 407 Text(item['title']) 408 ... 409 // 当flag为true时,width为15%;当flag为false时,width为95% 410 .width(this.flag ? '15%': '95%') 411 .animation({ duration: 2000, curve: item['curve'] }) 412 }, item => JSON.stringify(item)) 413 ``` 414 415### 完整代码 416```ts 417import Curves from '@ohos.curves'; 418 419@Entry 420@Component 421struct Index { 422 // 状态变量flag,用于监听按钮按下状态 423 @State flag: boolean = true 424 private items: object[] = [ 425 { 426 color: '#a320bf', 427 title: '线性曲线', 428 // curve类型曲线:表示动画从头到尾的速度都是相同的。 429 curve: Curve.Linear, 430 }, 431 { 432 color: '#17d4be', 433 title: '低速结束曲线', 434 // curve类型曲线:表示动画以低速结束 435 curve: Curve.EaseInOut, 436 }, 437 { 438 color: '#0e2d8c', 439 title: '平滑曲线', 440 // curve类型曲线:平滑曲线 441 curve: Curve.Smooth, 442 }, 443 { 444 color: '#d4bb19', 445 title: '弹簧曲线', 446 // ohos.curve导入的弹簧曲线动画,以初速度为20的弹簧进行平移 447 curve: Curves.springCurve(20, 1, 1, 1.2), 448 }, 449 ] 450 451 build() { 452 Flex({ 453 direction: FlexDirection.Column, 454 justifyContent: FlexAlign.Start, 455 alignItems: ItemAlign.Center 456 }) { 457 Column({ space: 50 }) { 458 ForEach(this.items, (item, index) => { 459 Text(item['title']) 460 .fontSize(20) 461 .fontColor('#FFFFFF') 462 // 当flag为true时,width为15%;当flag为false时,width为95%; 463 .width(this.flag ? '15%': '95%') 464 .height(80) 465 .textAlign(TextAlign.Start) 466 .backgroundColor(item['color']) 467 // 2s之内完成动画展示,每条动画曲线按照item['curve']去展示 468 .animation({ duration: 2000, curve: item['curve'] }) 469 .borderRadius(20) 470 .padding({left:10}) 471 }, item => JSON.stringify(item)) 472 } 473 .alignItems(HorizontalAlign.Start) 474 .width('100%') 475 .padding({ left: 12, right: 12, top: 22, bottom: 22 }) 476 477 Button(){ 478 Text("Go!") 479 .fontSize(20) 480 .fontColor('#fff') 481 } 482 .borderRadius(20) 483 .fontColor('#FFFFFF') 484 .fontSize('12fp') 485 .fontWeight(FontWeight.Bolder) 486 .backgroundColor('#15587c') 487 .padding(25) 488 .margin({top:50}) 489 // 通过点击事件,反转flag的值 490 .onClick(() => { 491 this.flag = !this.flag 492 }) 493 } 494 .height('100%') 495 .backgroundColor('#F1F3F5') 496 .padding({ left: '3%', right: '3%' }) 497 } 498} 499``` 500 501 502 503 504## 参考 505 506[属性动画](../application-dev/reference/apis-arkui/arkui-ts/ts-animatorproperty.md#属性动画) 507 508[Curve](../application-dev/reference/apis-arkui/arkui-ts/ts-appendix-enums.md#curve) 509 510[弹簧曲线动画](../application-dev/ui/arkts-spring-curve.md) 511 512[Flex布局](../application-dev/reference/apis-arkui/arkui-ts/ts-universal-attributes-flex-layout.md) 513 514[Listitem](../application-dev/reference/apis-arkui/arkui-ts/ts-container-listitem.md) 515 516[List](../application-dev/reference/apis-arkui/arkui-ts/ts-container-list.md)