圆形进度条/环形进度条 -- 最通俗易懂的讲解

1,748 阅读7分钟

一、前言

UI组件系列好久没有更新了,今天我们来分析一下常见的一种组件:环形进度条,也叫 圆形进度条。这种组件在被使用的时候又显的吃力,因为UI设计师在设计它的时候往往会增加一些元素,来显的高大尚一些。这种时候鸡肋点就显示出来了:

  • 往往组件库是不支持扩展的。
  • 为了让它高大尚 去引入相应的专属第三方库又显的没必要。
  • 圆形进度条组件在市面上有,但是很少有多端支持的。

所以为了解决上述痛点,我们不得不自己写一个这样的组件。如果你对它无从下手,或者思路不清晰,那么这篇文章正好适合你。

那就让我们一起let go 吧。

二、效果展示

10%的效果是这样的

环形进度条1.png

25%的效果是这样的:

环形进度条2.png

50%的效果是这样的

环形进度条3.png

75的效果是这样的

环形进度条5.png

三、实现前的说明

本次实现的功能说明:

  • 1、本篇文章使用 canvas 来绘画 圆形进度条组件。
  • 2、基本的环形进度条展示。
  • 3、定制环形进度条结尾处的元素。

万丈高楼平地起,我们本次文章主打的就是基础建设。只有基础建设的好,我们才能在此基础上做很多的扩展。

四、实现过程

4.1、准备画布

<style>
    div {
        width: 300px;
        height: 300px;
    }
    canvas {
        width: 100%;
        height: 100%;
    }
</style>
<div>
    <canvas id="canvas"></canvas>
    <script>
        function drawerCircle (){
            let canvas = document.getElementById('canvas');
            let elementBox = document.querySelector('div');
            canvas.width = elementBox.getBoundingClientRect().width;
            canvas.height = elementBox.getBoundingClientRect().height;
        }
    </script>
</div>

好了,第一个问题来了,我们给canvas设置大小有3种方式:
1、像上面这样,通过js来设置。
2、直接给canvas添加100%的行内样式。<canvas width="100%" height="100%"></canvas>
3、在样式表里设置canvas标签样式。
我们为什么不采用第二种、第三种方式?知道答案的小伙伴可在评论区里回答一下。

4.2 如何画圆圈?

canvas.png

这里引用了菜鸟教程的定义。上面的图主要传递了以下几点信息:

  1. sAngle、eAngle 传入的都是弧度,而不是角度。
  2. counterclockwise 代表的是绘画方向。
  3. 弧度与角度的关系:角度 = ( 180 * 弧度 ) / Math.PI
<style>
    div {
        width: '300px';
        height: '300px';
    }
</style>
<div>
    <canvas id='canvas'>当前浏览器不支持画布元素</canvas>
</div>
function drawerCicle(){
    let canvas = document.getElementById('canvas');
    canvas.width = document.querySelector('div')?.getBoundingClientRect()?.width;
    canvas.height = document.querySelector('div')?.getBoundingClientRect()?.height;
    // 1、确定组件的圆心位置
    let xCenter = canvas.width / 2;
    let yCenter = canvas.height / 2;
    // 2、拿到绘画上下文
    let ctx = canvas.getContext('2d');
    // 3、开启绘画路径
    ctx.beginPath();
    // 4、确定画笔颜色
    ctx.strokeStyle = '#ccc';
    // 5、设置画笔粗细
    ctx.lineWidth = 10;
    // 6、确定圆弧半径
    let radius = (canvas.width - ( 2 * ctx.lineWidth ) ) / 2;
    // 7、调用arc绘制圆弧
    ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI, false);
    // 8、将圆弧上的点连线
    ctx.stroke();
    // 9、关闭路径
    ctx.closePath();
}
drawerCicle();

根据代码,我们已经可以画出来圆圈了,如下图:

canvas1.png

4.3、在上一步的基础上,如何画进度?

其实就是重复第二步嘛,画2遍圆,然后重合一下就行了。我们先改动一点代码来验证我们的思路:

function drawerCicle(){
    let canvas = document.getElementById('canvas');
    canvas.width = document.querySelector('div')?.getBoundingClientRect()?.width;
    canvas.height = document.querySelector('div')?.getBoundingClientRect()?.height;
    // 1、确定组件的圆心位置
    let xCenter = canvas.width / 2;
    let yCenter = canvas.height / 2;
    // 2、拿到绘画上下文
    let ctx = canvas.getContext('2d');
    // 3、开启绘画路径
    ctx.beginPath();
    // 4、确定画笔颜色
    ctx.strokeStyle = '#ccc';
    // 5、设置画笔粗细
    ctx.lineWidth = 10;
    // 6、确定圆弧半径
    let radius = (canvas.width - ( 2 * ctx.lineWidth ) ) / 2;
    // 7、调用arc绘制圆弧
    ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI, false);
    // 8、将圆弧上的点连线
    ctx.stroke();
    // 9、关闭路径
    ctx.closePath();

    // 画进度弧 ===============================
    ctx.beginPath();
    // 4、确定画笔颜色
    ctx.strokeStyle = 'orange';
    // 5、设置画笔粗细
    ctx.lineWidth = 10;
    // 7、调用arc绘制圆弧
    ctx.arc(xCenter, yCenter, radius, 0, 0.5 * Math.PI, false);
    // 8、将圆弧上的点连线
    ctx.stroke();
    // 9、关闭路径
    ctx.closePath();
}

此时的结果如下:

canvas2.png

看来思路是对的,但就是有点瑕疵:

canvas3.png

问题a: 我们的圆弧应该是从板块1开始,而不是从板块4开始

问题b: 如何确定圆弧长度?

4.3.1、问题a

4.2小结 我们知道,对于一个圆弧来说,在中心点已知的情况下,起始弧、终止弧、绘画方向 决定了圆弧的形状。

我们现在画圆弧的代码是这个:

    ctx.arc(xCenter, yCenter, radius, 0, 0.5 * Math.PI, false);
    // 对于这段代码的解释如下:
    // 1、起始弧度0,根据弧度与角度的换算关系,得出起始角度为0度
    // 2、终止弧度0.5 * Math.PI,根据弧度与角度的换算关系,得出终止角度90度
    // 3、false代表顺时针绘画

所以,如果我们想要进度从第一板块开始,那么我们只需要调整 起始弧度终止弧度 即可。 调整代码如下:

    ctx.arc(xCenter, yCenter, radius, 1.5*Math.PI, 2 * Math.PI, false);

现在的样式如下:

canvas4.png

4.3.2、问题b

如何确定进度弧长?我们可以通过传参来确定,规定传参是小数或者百分比

一个圆弧的总弧长是2*Math.PI,so,2*Math.PI*传参就是我们的进度弧长。

因为我们的起始弧长是 1.5*Math.PI, so,我们的终止弧长就是:1.5*Math.PI + 2*Math.PI*传参

分析完毕,调整代码如下:

function drawerCicle(){
    // 规定进度条占比
    let percent = 0.1;
    
    //画基础圆弧代码不变...
    
    // 画进度圆弧
    ctx.beginPath();
    ctx.strokeStyle = 'orange';
    ctx.lineWidth = 10;
    ctx.arc(xCenter, yCenter, radius, 1.5*Math.PI, (1.5*Math.PI) + (percent * 2 * Math.PI), false);
    ctx.stroke();
    ctx.closePath();
}

现在的10%的现象如下:

canvas5.png

4.4、在进度的结尾,如何画圆球?

这个功能的难点在于如何得到圆球的中心坐标。在这里跟大家说个结论,(lineWidth = 12 上的圆弧里的点) === (lineWidth = 1 上的圆弧里的点)

canvas6.png

计算圆球中心逻辑如下:

  1. 如果进度对应的角度落在第1板块, 说明此时进度圆弧对应的角度A在(0, 90]区间。
  2. 如果进度对应的角度落在第4板块, 说明此时进度圆弧对应的角度A在(90, 180]区间。
  3. 如果进度对应的角度落在第3板块, 说明此时进度圆弧对应的角度A在(180, 270]区间。
  4. 如果进度对应的角度落在第2板块, 说明此时进度圆弧对应的角度A在(270, 360]区间。

我们不需要所有的板块都分析,我们只分析 第1板块,其他板块都类似。

第1板块

首先,我们需要求出进度对应的角度是多少。

这个很简单,从技术角度来看,进度占比percent是用户自定义的,同时也是一个必传的属性。假定我们现在要画20%的进度,那么此时的传参percent就是0.2。整个圆弧的角度360°,那么percent对应的角度就应该是0.2 * 360°

其次,假设弧度对应的角度是30°,如下图:

canvas5.png

这里再强调一下,注意到cd中间的黑点了吗,黑点坐标其实就是c点坐标。我们的圆球坐标就是黑点坐标。

也就是说如果我们以c为中心画圆,那么最终的位置效果就是以黑点为中心画圆。

根据上面的分析,我们可以写出如下函数来确定圆球的中心位置。

    // 确定圆球的中心位置
    /**
        1、BigCircleRadius:蓝色圆的半径,也就是上图的 `oa`
        2、Deg:进度圆弧对应的角度,也就是 `前文中的percent` * `360`
        3、Angle:进度的终止弧,我们能得到角度,再根据`角度与弧度的换算关系`得出终止弧长。结果:(( percent * 360  ) / 180) * Math.PI
        4、halfRect:蓝色正方形的width或者高度的一半,也就是上图中的 `ob`
        5、返回值:进度弧上终点的坐标
    */
    function surePosition(BigCircleRadius, Deg, Angle, halfRect){
        let result = {
                x: 0,
                y: 0
            }
        if (Deg > 0 && Deg <= 90){  // 第一象限
            let tx = halfRect + ((BigCircleRadius) * Math.sin(Angle));
            let ty = halfRect - ((BigCircleRadius) * Math.cos(Angle));
            return {
                x: tx,
                y: ty
            }
        }
    }

五、结尾

本篇文章到这里就结束啦,各位小伙伴还想看哪些组件的原生实现,欢迎评论区里点菜。886......