1# 水波纹动画开发
2
3## 场景介绍
4在日常应用场景中,水波纹的效果比较常见,例如数字键盘按键效果、听歌识曲、附近搜索雷达动效等等,本文就以数字按键为例介绍水波纹动效的实现。
5
6## 效果呈现
7本例最终效果图如下:
8
9![waterwaves](figures/waterwaves.gif)
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)