wx小程序中实现 圆形堆叠渐变进度条

712 阅读4分钟
本次分享是在实际业务中遇到的一个需求,如图下:

实现一个圆形进度条 要求展示三段颜色,和渐变颜色 在日常业务中,我们遇到了一些需求,需要实现一个 圆形进度条,并且该进度条需要展示 三段颜色,且颜色之间要 渐变过渡。最终的效果如下图所示:

需求细节:

• 进度条为圆形

• 进度条的颜色分为三段,第一段是前20%的绿色,第二段都需要平滑渐变,第三段是背景色

• 使用 小程序Canvas 实现

image.png

  1. 创建一个canvas对象 绑定一个id
<canvas id="progressCanvas" type="2d" style="width: 340rpx; height: 320rpx;"></canvas>

Canvas 元素将用于绘制圆形进度条。我们使用微信小程序 wx.createSelectorQuery() 获取 Canvas 的节点,并通过设置 pixelRatio 以适配高分屏设备。

2.这是我们定义的js文件,

  • 接收父组件传递 percent 表示进度条的百分比。默认为 0
  • 在生命周期 ready 中,dom节点渲染完后 来进行canvas的初始化操作
  • initCanvas 里做了:获取dom元素 画布尺寸,并设置他的宽高和 dpr(适应高分辨率屏幕)
  • 接下来在 drawProgressCircle 函数里面进行绘画

``

Component({
  properties: {
    percent: {
      type: Number,
      value: 0,
    },
  },

  lifetimes: {
    ready() {
      this.initCanvas();
    },
  },

  data: {
    canvas: null,
    ctx: null,
    canvasWidth: 200,  // 设置一个默认宽度
    canvasHeight: 200, // 设置一个默认高度
  },

  methods: {
    // 初始化 Canvas
    initCanvas() {
      const query = wx.createSelectorQuery().in(this);

      query
        .select('#progressCanvas')
        .fields({ node: true, size: true })
        .exec((res) => {
          const canvas = res[0].node;
          const ctx = canvas.getContext('2d');
          const dpr = wx.getSystemInfoSync().pixelRatio;

          // 获取 Canvas 的宽高并适配高分屏
          const canvasWidth = res[0].width * dpr;
          const canvasHeight = res[0].height * dpr;
          canvas.width = canvasWidth;
          canvas.height = canvasHeight;
          ctx.scale(dpr, dpr);

          this.setData({
            canvas,
            ctx,
            canvasWidth: res[0].width,
            canvasHeight: res[0].height,
          });

          // 初次绘制进度条
          this.drawProgressCircle(this.properties.percent || 0);
        });
    },

  },
});
  1. 定义一些常量
  • FULL_CIRCLE_ANGLE:表示完整的圆形角度,2 * Math.PI(360 度)。

  • lineWidth:圆环的宽度,14 像素。

  • radius:圆的半径,70 像素。

  • START_ANGLE_OFFSET:起始角度的偏移量,用于确定环形进度条的开始绘制位置。这里的 0.745 * Math.PI 大约是 135 度,使进度条从该角度开始绘制。

  • zoomSize:百分比的缩放基准值,用于计算进度条的终点角度。这里设为 132(可以根据自己需求来调)。

      // 绘制环形进度条
        drawProgressCircle(percent) {
          const ctx = this.data.ctx;
          if (!ctx) return;
    
          const FULL_CIRCLE_ANGLE = 2 * Math.PI; // 完整角度
          const lineWidth = 14; // 圆环的宽度
          const radius = 70; // 圆的半径
    
          // 获取 canvas 的中心坐标
          const centerX = this.data.canvasWidth / 2;
          const centerY = this.data.canvasHeight / 2;
          
          
          const START_ANGLE_OFFSET = 0.745 * Math.PI; // 起始角度偏移量    
          const zoomSize = 132; // percent 的缩放基准 满足拱形

          // 计算实际进度(percent)条终点角度
          const progressEndAngle = START_ANGLE_OFFSET + FULL_CIRCLE_ANGLE * (percent / zoomSize);
    
        }
  1. 绘制背景色 ctx.arc(centerX, centerY, radius, START_ANGLE_OFFSET, 0.25 * Math.PI, false)
  • (centerX, centerY)代表圆心的位置
  • radius 圆的半径
  • START_ANGLE_OFFSET 开始角度
  • 0.25 * Math.PI 结束角度
  • false 顺时针绘画
      // 首先清空画布 
      ctx.clearRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);
      // 绘制背景圆环
      ctx.lineWidth = lineWidth;
      ctx.strokeStyle = '#CADFDF';
      ctx.lineCap = 'round';
      ctx.beginPath();
      ctx.arc(centerX, centerY, radius, START_ANGLE_OFFSET, 0.25 * Math.PI, false);
      ctx.stroke();
  1. 绘制渐变色,这里咱们先绘制渐变色是因为 前20%绿色的层级要比渐变色高 ,注意canvas的层叠

image.png

      // 计算渐变段数和角度

      this.drawArcWithGradient(
        ctx,
        centerX,
        centerY,
        radius,
        START_ANGLE_OFFSET,
        progressEndAngle,
        percent,
      );
      
      下面这些函数 放在methods里 和
    // 分段渐变绘制函数
    drawArcWithGradient(
      ctx,
      centerX,
      centerY,
      radius,
      startAngle,
      endAngle,
      segments,
    ) {
      const segmentAngle = (endAngle - startAngle) / segments; // 每段的角度

      const startColor = '#b4d477'; // 起始颜色
      const middleColor = '#efcd2d'; // 中间颜色
      const endColor = '#e84033'; // 结束颜色

      const colors = this.generateGradientColors(
        startColor,
        middleColor,
        endColor,
        80,
      );

      for (let i = 0; i < segments; i++) {
        const segmentStart = startAngle + i * segmentAngle;
        const segmentEnd = segmentStart + segmentAngle;

        // 计算每一段的渐变颜色
        const progress = i / segments;
        const colorIndex = Math.floor(progress * (colors.length - 1));
        const nextColorIndex = colorIndex + 1;

        // 插值计算颜色过渡
        const ratio = (progress * (colors.length - 1)) % 1;
        const color = this.interpolateColor(
          colors[colorIndex],
          colors[nextColorIndex],
          ratio,
        );

        // 设置线的颜色
        ctx.strokeStyle = color;

        // 开始绘制
        ctx.beginPath();
        ctx.arc(centerX, centerY, radius, segmentStart, segmentEnd, false);
        ctx.stroke();
      }
    },
    // 颜色插值函数,用于过渡
    interpolateColor(color1, color2, ratio) {
      const hex = (x) => x.toString(16).padStart(2, '0');

      const r = Math.ceil(
        parseInt(color1.substring(1, 3), 16) * (1 - ratio) +
        parseInt(color2.substring(1, 3), 16) * ratio,
      );
      const g = Math.ceil(
        parseInt(color1.substring(3, 5), 16) * (1 - ratio) +
        parseInt(color2.substring(3, 5), 16) * ratio,
      );
      const b = Math.ceil(
        parseInt(color1.substring(5, 7), 16) * (1 - ratio) +
        parseInt(color2.substring(5, 7), 16) * ratio,
      );

      return `#${hex(r)}${hex(g)}${hex(b)}`;
    },
    // console.log(gradientColors);
    generateGradientColors(startColor, middleColor, endColor, numColors) {
      const colors = [];
      const midPoint = Math.floor(numColors / 2);

      // 插值从起始颜色到中间颜色
      for (let i = 0; i < midPoint; i++) {
        const ratio = i / (midPoint - 1);
        colors.push(this.interpolateColor(startColor, middleColor, ratio));
      }

      // 插值从中间颜色到结束颜色
      for (let i = 0; i < numColors - midPoint; i++) {
        const ratio = i / (numColors - midPoint - 1);
        colors.push(this.interpolateColor(middleColor, endColor, ratio));
      }

      return colors;
    },
  1. 绘制绿色部分
  • 先绘制一条与背景色同色的北京 ,当整体进度超过20的时候展示

image.png

     // 绘制白色的进度条(作为底部边框效果)
      const whitePercent = percent >= 20 ? 20.7 / zoomSize : 0;
      const whiteEndAngle = START_ANGLE_OFFSET + FULL_CIRCLE_ANGLE * whitePercent;
      ctx.strokeStyle = '#CADFDF';
      ctx.lineCap = 'round';
      ctx.beginPath();
      ctx.arc(centerX, centerY, radius, START_ANGLE_OFFSET, whiteEndAngle, false);
      ctx.stroke();

      // 绿色进度条(显示前 20%)
      const greenPercent = percent > 20 ? 20 / zoomSize : percent / zoomSize;
      const greenEndAngle = START_ANGLE_OFFSET + FULL_CIRCLE_ANGLE * greenPercent;
      ctx.strokeStyle = '#5cc57f';
      ctx.lineCap = 'round';
      ctx.beginPath();
      ctx.arc(centerX, centerY, radius, START_ANGLE_OFFSET, greenEndAngle, false);
      ctx.stroke();
  1. 点缀小白点(不知道该叫他什么) 只是个进度装饰 没有功能
  • 这里 开始角度和结束角度一定不要相同,在手机上会出现 不生效(不显示)的情况
  • progressEndAngle - 0.001

image.png

      // 白点
      ctx.lineWidth = lineWidth - 5;
      ctx.strokeStyle = '#FFF';
      ctx.beginPath();
      ctx.arc(centerX, centerY, radius, progressEndAngle - 0.001, progressEndAngle, false);
      ctx.stroke();

  1. 白色背景 image.png
      // 绘制白色的大圆点背景底色
      ctx.lineWidth = 106;
      ctx.strokeStyle = '#FFF';
      ctx.fillStyle = '#FFF';
      ctx.beginPath();
      ctx.arc(centerX, centerY, 10, 0, FULL_CIRCLE_ANGLE, false);
      ctx.fill();
      ctx.stroke();

      // 灰色点缀线
      ctx.lineWidth = 0.5;
      ctx.strokeStyle = 'rgba(0,0,0,0.2)';
      ctx.beginPath();
      ctx.arc(centerX, centerY, 56, 0, FULL_CIRCLE_ANGLE, false);
      ctx.stroke();
  1. 绘制文字

image.png

    // 绘制文字 80%
    ctx.textAlign = 'center';
    ctx.font = '22px Arial';
    ctx.fillStyle = 'rgba(0,0,0,1)';
    ctx.fillText(`${percent.toFixed(1)}%`, centerX, centerY + 15);

    // 绘制辅助文字
    ctx.font = '10px Arial';
    ctx.fillStyle = 'rgba(0,0,0,0.6)';
    ctx.fillText('已用费用比', centerX, centerY - 15);

源码地址 miniprogram-progress: 微信小程序 圆形进度堆叠渐变进度条 (gitee.com)