1# 水波纹动画开发 2 3## 场景介绍 4在日常应用场景中,水波纹的效果比较常见,例如数字键盘按键效果、听歌识曲、附近搜索雷达动效等等,本文就以数字按键为例介绍水波纹动效的实现。 5 6## 效果呈现 7本例最终效果图如下: 8 9 10 11## 环境要求 12本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: 13- IDE: DevEco Studio 3.1 Beta2 14- SDK: Ohos_sdk_public 3.2.11.9(API Version 9 Release) 15## 实现思路 16本实例涉及到的主要特性及其实现方案如下: 17* UI框架:使用Grid,GridItem等容器组件组建UI框架。 18* 按钮渲染:通过自定义numBtn组件(含Column、Button、Stack、Text等关键组件以及visibility属性),进行数字按钮的渲染。 19* 按钮状态变化:设置状态变量unPressed,控制按钮的当前状态,向Column组件添加onTouch事件,监听按钮的当前状态。 20 * 默认状态为按钮放开状态(unPressed为true)。 21 * 当按钮按下时,更新按钮的状态(unPressed:true -> false)。 22 * 当按钮放开时,更新按钮的状态(unPressed:false -> true)。 23* 按钮动画展示:使用属性动画以及组件内转场动画绘制按钮不同状态下的动画。 24 * 当按钮按下时,使用显式动画(animateTo)加载动画:插入按下时的Row组件,同时加载水波的聚拢效果。 25 * 当按钮放开时,使用组件内转场加载动画:插入放开时的Row组件,同时加载水波的扩散效果。 26## 开发步骤 27针对实现思路中所提到的内容,具体关键开发步骤如下: 281. 先通过Grid,GridItem等容器组件将UI框架搭建起来,在GuidItem中引用步骤2中的自定义数字按钮numBtn构建出数字栅格。 29 30 具体代码如下: 31 32 ```ts 33 private numGrid: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, -1, 0, -1] 34 ... 35 Column() { 36 Grid() { 37 ForEach(this.numGrid, (item: number, index: number) => { 38 GridItem() { 39 ... 40 } 41 }, item => item) 42 } 43 .columnsTemplate('1fr 1fr 1fr') 44 .rowsTemplate('1fr 1fr 1fr 1fr') 45 .columnsGap(10) 46 .rowsGap(10) 47 .width(330) 48 .height(440) 49 }.width('100%').height('100%') 50 ``` 51 522. 通过Column、Button、Stack、Text等关键组件以及visibility属性构建自定义数字按钮numBtn。 53 54 具体代码如下: 55 56 ```ts 57 @Component 58 struct numBtn { 59 ··· 60 build() { 61 Column() { 62 Button() { 63 stack(){ 64 ... 65 Text(`${this.item}`).fontSize(30) 66 } 67 ... 68 } 69 .backgroundColor('#ccc') 70 .type(ButtonType.Circle) 71 .borderRadius(100) 72 .width(100) 73 .height(100) 74 } 75 .visibility(this.item == -1 ? Visibility.Hidden : Visibility.Visible) 76 .borderRadius(100) 77 } 78 } 79 ``` 80 813. 设置状态变量unPressed,监听当前数字按钮的状态,同时向Column组件添加onTouch事件,获取并更新按钮的当前状态,从而可以根据监听到的按钮状态加载对应的动画效果。 82 83 具体代码块如下: 84 85 ```ts 86 //状态变量unPressed,用于监听按钮按下和放开的状态 87 @State unPressed: boolean = true 88 ... 89 // 添加onTouch事件,监听状态 90 .onTouch((event: TouchEvent) => { 91 // 当按钮按下时,更新按钮的状态(unPressed:true -> false) 92 if (event.type == TouchType.Down) { 93 animateTo({ duration: 400 }, () => { 94 this.unPressed = !this.unPressed 95 this.currIndex = this.index 96 }) 97 } 98 // 当按钮放开时,更新按钮的状态(unPressed:false -> true) 99 if (event.type == TouchType.Up) { 100 animateTo({ duration: 400 }, () => { 101 this.unPressed = !this.unPressed 102 }) 103 } 104 }) 105 ``` 106 1074. 根据按钮组件的按下/放开状态,通过if-else语句选择插入的Row组件,并随之呈现不同的水波动画效果(按下时水波聚拢,放开时水波扩散)。 108 109 具体代码块如下: 110 111 ```ts 112 Stack() { 113 Row() { 114 // 判断当前按钮组件为放开状态 115 if (this.unPressed && this.currIndex == this.index) { 116 // 插入Row组件,配置过渡效果 117 Row() 118 .customStyle() 119 .backgroundColor('#fff') 120 // 水波纹扩散动画:从Row组件的中心点开始放大,scale{0,0}变更scale{1,1}(完整显示) 121 .transition({ 122 type: TransitionType.Insert, 123 opacity: 0, 124 scale: { x: 0, y: 0, centerY: '50%', centerX: '50%' } 125 }) 126 } 127 // 判断当前按钮组件为按下状态 128 else if (!this.unPressed && this.currIndex == this.index) { 129 // 插入Row组件,配置过渡效果 130 Row() 131 .customStyle() 132 .backgroundColor(this.btnColor) 133 .scale(this.btnScale) 134 .onAppear(() => { 135 // 水波纹聚拢动画:Row组件backgroundColor属性变更(#ccc -> #fff),插入动画过渡效果,scale{1,1}(完整显示)变化为scale{0,0} 136 animateTo({ duration: 300, 137 // 聚拢动画播放完成后,需要衔接扩散动画,Row组件backgroundColor属性变更(#fff -> #ccc),插入动画过渡效果,scale{0,0}变化为scale{1,1}(完整显示) 138 onFinish: () => { 139 this.btnColor = '#ccc' 140 this.btnScale = { x: 1, y: 1 } 141 } }, 142 () => { 143 this.btnColor = '#fff' 144 this.btnScale = { x: 0, y: 0 } 145 }) 146 }) 147 } 148 // 其他状态 149 else { 150 Row() 151 .customStyle() 152 .backgroundColor('#fff') 153 } 154 } 155 .justifyContent(FlexAlign.Center) 156 .alignItems(VerticalAlign.Center) 157 .borderRadius(100) 158 Text(`${this.item}`).fontSize(30) 159 } 160 .customStyle() 161 ``` 162 163## 完整代码 164示例代码如下: 165 166```ts 167@Entry 168@Component 169export default struct dragFire { 170 private numGrid: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, -1, 0, -1] 171 172 build() { 173 Column() { 174 Grid() { 175 ForEach(this.numGrid, (item: number, index: number) => { 176 GridItem() { 177 numBtn({ item: item, index: index }) 178 } 179 }, item => item) 180 } 181 .columnsTemplate('1fr 1fr 1fr') 182 .rowsTemplate('1fr 1fr 1fr 1fr') 183 .columnsGap(10) 184 .rowsGap(10) 185 .width(330) 186 .height(440) 187 }.width('100%').height('100%') 188 } 189} 190 191 192@Component 193struct numBtn { 194 private currIndex: number = -1 195 //状态变量unPressed,用于控制按钮的状态 196 @State unPressed: boolean = true 197 @State btnColor: string = '#ccc' 198 index: number 199 item: number 200 @State btnScale: { 201 x: number, 202 y: number 203 } = { x: 1, y: 1 } 204 205 @Styles customStyle(){ 206 .width('100%') 207 .height('100%') 208 .borderRadius(100) 209 } 210 211 build() { 212 Column() { 213 Button() { 214 Stack() { 215 Row() { 216 // 判断当前组件为放开状态 217 if (this.unPressed && this.currIndex == this.index) { 218 // 插入Row组件,配置过渡效果 219 Row() 220 .customStyle() 221 .backgroundColor('#fff') 222 // 水波纹扩散动画:Row组件backgroundColor属性变更(#fff -> #ccc),系统插入动画过渡效果,从组建的中心点开始放大,scale{0,0}变更scale{1,1} 223 .transition({ 224 type: TransitionType.Insert, 225 opacity: 0, 226 scale: { x: 0, y: 0, centerY: '50%', centerX: '50%' } 227 }) 228 } 229 // 判断当前组件为按下状态 230 else if (!this.unPressed && this.currIndex == this.index) { 231 // 插入Row组件,配置过渡效果 232 Row() 233 .customStyle() 234 .backgroundColor(this.btnColor) 235 .scale(this.btnScale) 236 .onAppear(() => { 237 // 水波纹聚拢动画:Row组件backgroundColor属性变更(#ccc -> #fff),插入动画过渡效果,scale{1,1}变化为scale{0,0} 238 animateTo({ duration: 300, 239 // 聚拢动画播放完成后,需要衔接扩散动画,此时Row组件backgroundColor属性变更(#fff -> #ccc),插入动画过渡效果,scale{0,0}变化为scale{1,1} 240 onFinish: () => { 241 this.btnColor = '#ccc' 242 this.btnScale = { x: 1, y: 1 } 243 } }, 244 () => { 245 this.btnColor = '#fff' 246 this.btnScale = { x: 0, y: 0 } 247 }) 248 }) 249 } 250 // 其他状态 251 else { 252 Row() 253 .customStyle() 254 .backgroundColor('#fff') 255 } 256 } 257 .justifyContent(FlexAlign.Center) 258 .alignItems(VerticalAlign.Center) 259 .borderRadius(100) 260 261 Text(`${this.item}`).fontSize(30) 262 } 263 .customStyle() 264 } 265 .stateEffect(false) 266 .backgroundColor('#ccc') 267 .type(ButtonType.Circle) 268 .borderRadius(100) 269 .width(100) 270 .height(100) 271 } 272 .visibility(this.item == -1 ? Visibility.Hidden : Visibility.Visible) 273 .borderRadius(100) 274 // onTouch事件,监听状态 275 .onTouch((event: TouchEvent) => { 276 // 当按钮按下时,更新按钮的状态(unPressed:true -> false) 277 if (event.type == TouchType.Down) { 278 animateTo({ duration: 400 }, () => { 279 this.unPressed = !this.unPressed 280 this.currIndex = this.index 281 }) 282 } 283 // 当按钮放开时,更新按钮的状态(unPressed:false -> true) 284 if (event.type == TouchType.Up) { 285 animateTo({ duration: 400 }, () => { 286 this.unPressed = !this.unPressed 287 }) 288 } 289 }) 290 } 291} 292``` 293## 参考 294[Grid](../application-dev/reference/apis-arkui/arkui-ts/ts-container-grid.md) 295 296[GridItem](../application-dev/reference/apis-arkui/arkui-ts/ts-container-griditem.md) 297 298[显式动画](../application-dev/reference/apis-arkui/arkui-ts/ts-explicit-animation.md) 299 300[组件内转场](../application-dev/reference/apis-arkui/arkui-ts/ts-transition-animation-component.md) 301 302[Stack](../application-dev/reference/apis-arkui/arkui-ts/ts-container-stack.md)