Circle显示不同进度水波纹
===============

#### 问题现象

如何在圆形区域内展示不同加载进度，并在其中心采用类似水波纹的效果？

#### 效果预览

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/99/v3/N4bdXvXnTIy9e0V3dBYA7Q/zh-cn_image_0000002658913725.png?HW-CC-KV=V1&HW-CC-Date=20260709T134052Z&HW-CC-Expire=86400&HW-CC-Sign=C4F030DD89BCC4D371A578938E2CFE18EBBF0D07B0241B174AB3D1CDC74C5DD3 "点击放大")

#### 背景知识

*   [Circle](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-drawing-components-circle)组件用于绘制圆形，支持设置填充区域的颜色、边框颜色等特性。
*   [Path](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-drawing-components-path)组件可实现路径绘制，根据绘制路径生成封闭的自定义形状。
*   圆的标准方程(x-a)²+(y-b)²=r²中，有三个参数a、b、r，即圆心坐标为(a,b)，只要求出a、b、r，这时圆的方程就被确定，因此确定圆方程，需三个独立条件，其中圆心坐标是圆的定位条件，半径是圆的定形条件。

#### 解决方案

1.  以Stack组件作为容器，分层次地绘制各个部分，首先使用Circle组件来绘制外部的圆环。
    
    Circle({ width: BIG\_DIAMETER, height: BIG\_DIAMETER })
      .fill(COLOR\_TRANSPARENT)
      .stroke('#007DFF')
      .strokeWidth(5);
    
2.  在绘制中间进度的填充时，中间填充具有两个状态：
    *   当进度为100%时，显示为填充颜色的圆形。
    *   当进度不是100%时，使用Path组件绘制闭合曲线以实现填充效果。在使用Path组件绘制时的计算过程及相关函数的应用中，Path组件的坐标原点位于左上角。Path组件在内部计算路径时以px为单位，因此需要进行单位转换。
        *   进度百分比k和y的关系：y =（1 - k）\*2r。（剩余进度百分比乘以圆形的直径）。
        *   圆心点的坐标是（r， r），使用圆方程就可以计算出圆弧的起点和终点。
            
            calPathCommands(value: number): string {
              let y \= this.calY(value / 100.0);
              let squareX \= this.calXSquare(y);
              if (squareX \>\= 0) {
                let x \= Math.sqrt(squareX);
                let x1 \= this.RADIUS\_IN\_PX \- x;
                let x2 \= this.RADIUS\_IN\_PX \+ x;
                return this.formatPathCommands(x1, x2, y, this.RADIUS\_IN\_PX);
              }
              return '';
            }
            
        *   使用Path的[commands](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-drawing-components-path#commands)设置符合[SVG路径描述规范](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-drawing-components-path#svg路径描述规范)的命令字符串，单位为px。
            
            formatPathCommands(x1: number, x2: number, y: number, radius: number) {
              return \`M${x1} ${y} A${radius} ${radius} 0 ${y \> this.RADIUS\_IN\_PX ? 0 : 1} 0  ${x2} ${y} \` +
                \`Q${(x1 \+ 3 \* x2) / 4} ${y \+ 12.5 \* (x2 \- x1) / radius}, ${(x1 \+ x2) / 2} ${y} T${x1} ${y}\`;
            }
            
3.  绘制最上层的百分比显示，可以直接采用Text控件来实现。
    
    Text(this.outSetValue.toFixed(0) \+ '%')
      .fontSize(60);
    
4.  完整示例参考如下：
    
    const COLOR\_TRANSPARENT \= '#00000000';
    const COLOR\_BACKGROUND\_FILL \= '#007DFF';
    const DIAMETER \= 200;
    const BIG\_DIAMETER \= 220;
    
    @Entry
    @Component
    struct WaterRipplePage {
      @State outSetValue: number \= 50;
      @State pathCommands: string \= '';
      @State backGroundColor: string \= '#00000000';
      RADIUS\_IN\_PX: number \= this.getUIContext().vp2px(DIAMETER / 2.0);
    
      onPageShow() {
        this.pathCommands \= this.calPathCommands(this.outSetValue);
      }
    
      calXSquare(y: number) {
        return this.RADIUS\_IN\_PX \* this.RADIUS\_IN\_PX \- (y \- this.RADIUS\_IN\_PX) \* (y \- this.RADIUS\_IN\_PX);
      }
    
      calY(k: number) {
        return (1 \- k) \* this.RADIUS\_IN\_PX \* 2;
      }
      formatPathCommands(x1: number, x2: number, y: number, radius: number) {
        return \`M${x1} ${y} A${radius} ${radius} 0 ${y \> this.RADIUS\_IN\_PX ? 0 : 1} 0  ${x2} ${y} \` +
          \`Q${(x1 \+ 3 \* x2) / 4} ${y \+ 12.5 \* (x2 \- x1) / radius}, ${(x1 \+ x2) / 2} ${y} T${x1} ${y}\`;
      }
      calPathCommands(value: number): string {
        let y \= this.calY(value / 100.0);
        let squareX \= this.calXSquare(y);
        if (squareX \>\= 0) {
          let x \= Math.sqrt(squareX);
          let x1 \= this.RADIUS\_IN\_PX \- x;
          let x2 \= this.RADIUS\_IN\_PX \+ x;
          return this.formatPathCommands(x1, x2, y, this.RADIUS\_IN\_PX);
        }
        return '';
      }
      build() {
        Column() {
          Column() {
            Stack() {
           _   // 外框圆环_
              Circle({ width: BIG\_DIAMETER, height: BIG\_DIAMETER })
                .fill(COLOR\_TRANSPARENT)
                .stroke('#007DFF')
                .strokeWidth(5);
           _   // 进度显示_
              Circle({ width: DIAMETER, height: DIAMETER })
                .fill(this.backGroundColor);
              Path()
                .width(DIAMETER)
                .height(DIAMETER)
                .stroke('#007DFF')
                .commands(this.pathCommands)
                .fill(COLOR\_BACKGROUND\_FILL);
            _  // 进度_
              Text(this.outSetValue.toFixed(0) \+ '%')
                .fontSize(60);
            }.width(BIG\_DIAMETER)
            .height(BIG\_DIAMETER);
    
            Row() {
              Slider({
                value: this.outSetValue,
                min: 0,
                max: 100,
                step: 1,
                style: SliderStyle.OutSet
              })
                .blockColor('#FFFFFF')
                .trackColor('#05000000')
                .selectedColor('#007DFF')
                .showSteps(true)
                .showTips(true)
                .onChange((value: number) \=\> {
                  this.outSetValue \= value;
                  if (this.outSetValue \=== 100) {
                    this.backGroundColor \= COLOR\_BACKGROUND\_FILL;
                    this.pathCommands \= '';
                  } else {
                    this.backGroundColor \= COLOR\_TRANSPARENT;
                    this.pathCommands \= this.calPathCommands(this.outSetValue);
                  }
                });
            }
            .padding({ top: 50 })
            .width('80%');
          }.width('100%');
        }
        .height('100%')
        .justifyContent(FlexAlign.Center);
      }
    }