1# 如何实现波纹进度条 2## 场景说明 3应用开发过程中经常用到波纹进度条,常见的如充电进度、下载进度、上传进度等,本例即为大家介绍如何实现上述场景。 4## 效果呈现 5本示例最终效果如下: 6 7 8 9## 运行环境 10本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: 11- IDE: DevEco Studio 3.1 Beta2 12- SDK: Ohos_sdk_public 3.2.11.9(API Version 9 Release) 13 14## 实现思路 15本示例涉及4个主要特性及其实现方案如下: 16- 进度条的圆形外框:使用Circle组件绘制外层圆环,实现外层框架。 17- 圆框内进度数值变化:使用setInterval()让进度值持续递增控制进度数据变化(本案例未提供实时数据来源,所以直接通过数据递增来呈现效果)。 18- 圆框水纹区域绘制:通过Path组件的绘制命令(M、Q、T)去绘制水纹形状并对中间进行填充。 19- 底部进度条展示(主要用于跟波纹进度对比展示,方便大家理解):使用Slider组件绘制进度条。 20 21## 开发步骤 22针对上述所提到的内容,具体实现步骤如下: 231. 先使用Cricle组件绘制外层的圆环 24 具体代码块如下: 25 ```ts 26 ... 27 // 外框圆环 28 Circle({ width: BIG_DIAMETER, height: BIG_DIAMETER }) 29 .fill(COLOR_TRANSPARENT) // 填充:透明 30 .stroke('#007DFF') //圆环颜色 31 .strokeWidth(5) //圆环环宽 32 ... 33 ``` 342. 通过setInterval()方法让outSetValue值一直增加到100,使进度在规定时间内完成,最后通过clearInterval结束自增。 35 具体代码块如下: 36 ```ts 37 ... 38 aboutToAppear() { 39 this.test() 40 } 41 test() { 42 let timer = setInterval(() => { //开始定时 43 if (this.outSetValue < 100) { 44 //进度值每次+1 45 this.outSetValue += 1 46 //进度显示 47 if (this.outSetValue == 100) { 48 this.backGroundColor = COLOR_BACKGROUND_FILL 49 this.pathCommands = ''; 50 } else { 51 this.backGroundColor = COLOR_TRANSPARENT 52 this.pathCommands = this.calPathCommands(this.outSetValue); 53 } 54 } else { 55 clearInterval(timer) //取消定时 56 } 57 }, 100) 58 } 59 ... 60 ``` 61 623. 通过方程表达进度百分比和y的关系,通过Path组件的路径绘制命令(M、Q、T)去绘制路径生成封闭的自定义形状并对中间进行填充。 63 64 中间的填充有两个状态: 65 66 1.在进度100%时时填充颜色的圆形。 67 68 2.在进度不是100%时,使用Path组件绘制闭合曲线实现。 69 70 在使用Path组件绘制路径时的计算过程和相关函数的使用如下(坐标系以Path组件的左上角为坐标原点): 71 72  73 74 - 进度百分比和y的关系:y = (1-k)* 2r。 75 - 圆心点的坐标是(r, r),使用圆方程就可以计算出圆弧的起点和终点。 76 - 使用 A(rx ry x-axis-rotation large-arc-flag sweep-flag x y) 绘制圆弧,注意点就是在过圆心之后,需要反转下绘制角度。 77 - 使用 Q(x1 y1 x y) 和 T(x, y) 绘制对应的波浪,最后闭合路径然后填充颜色。 78 具体代码块如下: 79 ```ts 80 ... 81 onPageShow() { 82 //校准的路径指令与进度值 83 this.pathCommands = this.calPathCommands(this.outSetValue); 84 } 85 86 calXSquare(y: number) { 87 return RADIUS_IN_PX * RADIUS_IN_PX - (y - RADIUS_IN_PX) * (y - RADIUS_IN_PX); 88 } 89 90 calY(k: number) { 91 return (1 - k) * RADIUS_IN_PX * 2;//返回值为百分比 92 } 93 formatPathCommands(x1: number, x2: number, y: number, radius: number) { 94 //填充区域波浪线 95 return `M${x1} ${y} A${radius} ${radius} 0 ${y > RADIUS_IN_PX ? 0 : 1} 0 ${x2} ${y} ` 96 + `Q${(x1 + 3 * x2) / 4} ${y + 12.5 * (x2 - x1) / radius}, ${(x1 + x2) / 2} ${y} T${x1} ${y}` 97 } 98 calPathCommands(value: number) { 99 let y = this.calY(value / 100.0) 100 let squareX = this.calXSquare(y) 101 if (squareX >= 0) { 102 let x = Math.sqrt(squareX); 103 let x1 = RADIUS_IN_PX - x; 104 let x2 = RADIUS_IN_PX + x; 105 return this.formatPathCommands(x1, x2, y, RADIUS_IN_PX); 106 } 107 return ""; 108 } 109 ... 110 ``` 1114. 绘制下方滑动条组件 112 具体代码块如下: 113 ```ts 114 ... 115 Row() { 116 Slider({ 117 value: this.outSetValue, 118 min: 0, 119 max: 100, 120 step: 1, 121 style: SliderStyle.OutSet 122 }) 123 .blockColor('#FFFFFF') 124 .trackColor('#182431') 125 .selectedColor('#007DFF') 126 .showSteps(true) 127 .showTips(true) 128 .onChange((value: number, mode: SliderChangeMode) => { 129 if(this.outSetValue == 0) { 130 this.test() 131 } 132 this.outSetValue = value //初始状态 133 if (this.outSetValue == 100) { 134 this.backGroundColor = COLOR_BACKGROUND_FILL //进度为100时,滑动条拉满,背景全满 135 this.pathCommands = ''; 136 } else { 137 this.backGroundColor = COLOR_TRANSPARENT 、 138 this.pathCommands = this.calPathCommands(this.outSetValue); 139 } 140 console.log(`value = ${value} ->` + this.pathCommands); 141 //进度显示 142 }) 143 Text(this.outSetValue.toFixed(0)).fontSize(16) 144 } 145 ... 146 ``` 147 148## 完整代码 149具体代码如下: 150```ts 151const COLOR_TRANSPARENT = '#00000000' 152const COLOR_BACKGROUND_FILL = '#7ebede' 153 154const DIAMETER = 200; 155const RADIUS_IN_PX = vp2px(DIAMETER / 2.0); 156const BIG_DIAMETER = 220; 157 158 159@Entry 160@Component 161struct Page3 { 162 @State outSetValue: number = 0 163 @State pathCommands: string = '' 164 @State backGroundColor: string = '#00000000' 165 166 onPageShow() { 167 this.pathCommands = this.calPathCommands(this.outSetValue); 168 } 169 170 calXSquare(y: number) { 171 return RADIUS_IN_PX * RADIUS_IN_PX - (y - RADIUS_IN_PX) * (y - RADIUS_IN_PX); 172 } 173 174 calY(k: number) { 175 return (1 - k) * RADIUS_IN_PX * 2; 176 } 177 178 formatPathCommands(x1: number, x2: number, y: number, radius: number) { 179 return `M${x1} ${y} A${radius} ${radius} 0 ${y > RADIUS_IN_PX ? 0 : 1} 0 ${x2} ${y} ` 180 + `Q${(x1 + 3 * x2) / 4} ${y + 12.5 * (x2 - x1) / radius}, ${(x1 + x2) / 2} ${y} T${x1} ${y}` 181 } 182 183 calPathCommands(value: number) { 184 let y = this.calY(value / 100.0) 185 let squareX = this.calXSquare(y) 186 if (squareX >= 0) { 187 let x = Math.sqrt(squareX); 188 let x1 = RADIUS_IN_PX - x; 189 let x2 = RADIUS_IN_PX + x; 190 return this.formatPathCommands(x1, x2, y, RADIUS_IN_PX); 191 } 192 return ""; 193 } 194 195 aboutToAppear() { 196 this.test() 197 } 198 test() { 199 let timer = setInterval(() => { 200 if (this.outSetValue < 100) { 201 this.outSetValue += 1 202 if (this.outSetValue == 100) { 203 this.backGroundColor = COLOR_BACKGROUND_FILL 204 this.pathCommands = ''; 205 } else { 206 this.backGroundColor = COLOR_TRANSPARENT 207 this.pathCommands = this.calPathCommands(this.outSetValue); 208 } 209 } else { 210 clearInterval(timer) 211 } 212 }, 100) 213 } 214 build() { 215 216 Column() { 217 Column() { 218 Stack() { 219 // 外框圆环 220 Circle({ width: BIG_DIAMETER, height: BIG_DIAMETER }) 221 .fill(COLOR_TRANSPARENT) 222 .stroke('#007DFF') 223 .strokeWidth(5) 224 // 进度显示 225 226 Circle({ width: DIAMETER, height: DIAMETER }) 227 .fill(this.backGroundColor) 228 Path() 229 .width(DIAMETER) 230 .height(DIAMETER) 231 .commands(this.pathCommands) 232 .fill(COLOR_BACKGROUND_FILL) 233 234 // 进度 235 Text(this.outSetValue.toFixed(0) + "%") 236 .fontSize(60) 237 238 239 }.width(BIG_DIAMETER) 240 .height(BIG_DIAMETER) 241 242 243 Row() { 244 Slider({ 245 value: this.outSetValue, 246 min: 0, 247 max: 100, 248 step: 1, 249 style: SliderStyle.OutSet 250 }) 251 .blockColor('#FFFFFF') 252 .trackColor('#182431') 253 .selectedColor('#007DFF') 254 .showSteps(true) 255 .showTips(true) 256 .onChange((value: number, mode: SliderChangeMode) => { 257 if(this.outSetValue == 0) { 258 this.test() 259 } 260 this.outSetValue = value 261 if (this.outSetValue == 100) { 262 this.backGroundColor = COLOR_BACKGROUND_FILL 263 this.pathCommands = ''; 264 } else { 265 this.backGroundColor = COLOR_TRANSPARENT 266 this.pathCommands = this.calPathCommands(this.outSetValue); 267 } 268 console.log(`value = ${value} ->` + this.pathCommands); 269 }) 270 Text(this.outSetValue.toFixed(0)).fontSize(16) 271 } 272 .padding({ top: 50 }) 273 .width('80%') 274 275 }.width('100%') 276 } 277 .height('100%') 278 .justifyContent(FlexAlign.Center) 279 } 280} 281``` 282## 参考 283[Circle](../application-dev/reference/apis-arkui/arkui-ts/ts-drawing-components-circle.md) 284 285[Path](../application-dev/reference/apis-arkui/arkui-ts/ts-drawing-components-path.md) 286 287[Slider](../application-dev/reference/apis-arkui/arkui-ts/ts-basic-components-slider.md) 288 289[Timer](../application-dev/reference/common/js-apis-timer.md) 290