用CSS+SVG做一个优雅的环形进度条

开门见山

先上最终效果图:(Demo 传送门)

其中进度、尺寸、环宽和颜色都可以非常方便地进行控制。

核心原理:

利用两个重叠的圆环形,通过对上层圆环弧长的控制来表示进度,下层圆环则作为辅助,呈现环形进度条剩余的部分。

核心知识点:

  • SVG circle stroke-dasharray
  • 弧长公式 l = πrα/180°
  • CSS 变量
  • CSS 计数器

下面分享下具体实现过程。

实现环形

要实现环形,有多种技术可供选择,包括 Canvas、SVG,甚至 CSS + HTML 的组合。在本文中,我们使用了 SVG 方案,一来是因为 SVG 的 API 丰富,图形表现力非常强大,二来是 SVG 可以与 CSS 无缝搭配使用,实现更加强大的功能。

用 SVG 中的 circle 标签可以毫无压力的绘出一个直径 200px 的圆环:

html 复制代码
<svg width="400" height="400">
  <circle 
    cx="200" 
    cy="200" 
    r="100" 
    fill="none" 
    stroke="blue" 
    stroke-width="10" />
</svg>

我们再调整下代码,让圆环图形完美适应 SVG 容器的 200×200 大小:

html 复制代码
<svg width="200" height="200">
  <circle 
    cx="100" 
    cy="100"
    r="95"
    fill="none" 
    stroke="blue" 
    stroke-width="10" />
</svg>

小技巧 :为了完美适应容器尺寸,我们可以将半径 r 的值设置为容器宽度的一半减去 stroke-width 大小的一半,这样做可以确保圆环不会因为溢出而被容器剪裁掉。此处 r = 200 / 2 - 10 / 2,即 (200 - 10) / 2 = 95

接着使用相同的方法绘制另一个圆环,作为辅助圆环。为了视觉效果,进度圆环应该在上层,因此在代码中,进度圆环的标签应该放在辅助圆环的后面。完成后的代码如下:

html 复制代码
<svg width="200" height="200">
  <!-- 辅助圆环 -->
  <circle 
    cx="100" 
    cy="100" 
    r="95" 
    fill="none" 
    stroke="#ccc" 
    stroke-width="10" />
  
  <!-- 进度圆环 -->
  <circle 
    cx="100" 
    cy="100" 
    r="95" 
    fill="none" 
    stroke="blue" 
    stroke-width="10" />
</svg>

可以发现代码中有很多重复的属性,为了让代码更加简洁和高效,我们可以使用 CSS 将这些重复的部分提取出来,统一声明,让代码变得更加"干"(DRY):

css 复制代码
.progress-circle {
  width: 200px;
  height: 200px;
}

.progress-circle > circle {
  cx: 100px;
  cy: 100px;
  r: 95px;
  fill: none;
  stroke-width: 10px;
}
html 复制代码
<svg class="progress-circle">
  <circle stroke="#ccc" />
  <circle stroke="blue" />
</svg>

实现圆环进度

上下两层的圆环已经准备好了,现在的重点是如何实现上层圆环上的进度,这个问题可以分为2个关键点:

  1. 如何实现圆环的弧长?
  2. 如何将进度百分比转换为圆环的弧长?

要解决第1个问题,这里要用到 SVG 中的 stroke-dasharray 属性,这是一个用来控制路径虚线疏密程度的属性,其值是一组描述虚线的短划线与空白间隙长度的数列。例如,如果设置 stroke-dasharray="5 2",则路径将以 5 个像素的短划线和 2 个像素的空白间隙交替显示,其中第一个数控制短划线长度,第二个数控制空白间隙长度。

stroke-dasharray 的参数值还支持多个数列,详情见 MDN 文档

很明显,这里我们需要控制圆弧的长度(即虚线中的短划线长度)来呈现进度。然而,由于虚线中的短划线是多个且重复的,仅仅改变短划线长度并不能满足我们的需求,具体情况如下图所示:

根据需求,我们只需要圆环的 一段 圆弧即可,那如何实现呢?经过多次尝试,我们发现当改变虚线中空白间隙的长度(即 stroke-dasharray 的第二个数),当这个长度超过圆环的周长时,视觉效果上圆环只剩下一条独立的圆弧了,此时我们可以通过调整第一个数来改变圆弧的长度,从而解决了上面第1个问题。

上面第2个问题暂时看起来比较棘手,因为我们很难看出0%~100%之间的进度到底对应多长的弧长。我们继续探究,寻找规律。

根据需求,当进度百分比是100%时,进度条的圆弧要呈现出一个圆环,此时圆弧夹角是360°,当进度百分比是50%时,圆弧是个半圆环,夹角是180° 。反过来也成立:180°表示50%,360°表示100% 。可以看出进度百分比和角度是存在等量关系的,同时根据弧长公式 l = πrα/180°,带入角度就可以求出弧长了,至此"进度百分比 - 角度 - 弧长"三者的规律就清晰了,思路马上要通了。

l = πrα/180°

其中 l 表示弧长,π 是圆周率,r 表示半径,α 表示夹角

在实际使用中,我们是用百分数来控制弧长的,而不是用角度,所以接下来把进度百分比和弧长关联起来。

我们把弧长公式变动下,分子分母同时乘以2,则有:

l = 2πrα/180°*2,即 l = 2πr * α/360°

同时根据前面可知进度百分比和角度存在等量关系(360° 等于 100%),所以可以得到:

l = 2πr * p/100,其中 p 为当前进度百分数。

现在,我们就可以根据进度百分比来计算出对应的弧长了。

优化细节

环形进度条通常都是从12点钟方向开始的,而 SVG 中默认是3点钟方向作为起点,所以我们给它偏转-90°进行修正:

css 复制代码
.progress-circle {
  ...
  transform: rotate(-90deg);
}

另外我们发现圆弧的端点处过于生硬,给个圆角效果修饰下是个好主意。这里我们用了 SVG 中的属性 stroke-linecap

css 复制代码
.progress-circle > circle {
  ...
  stroke-linecap: round;
}

当然了,动画过渡效果也可以安排上,让进度条的变化更加丝滑:

css 复制代码
.progress-circle > circle {
  ...
  transition: stroke-dasharray 0.4s linear, stroke .3s;
}

组件化

环形进度条的效果虽然已经实现了,但光靠上面的那些代码还是很难复用。环形进度条的宽度、高度、半径、颜色等都是写死的,另外 stroke-dasharray 的值还得靠 JS 进行计算,再赋值给 <circle> 元素。说好的纯 CSS 实现呢?

要解决这些问题,我们需要将环形进度条组件化。

先定义一些组件全局要用到的 CSS 变量:

css 复制代码
/* 容器 */
.progress-circle {
  --percent: 0;  /* 百分数 */
  --size: 180px;  /* 尺寸大小 */
  --border-width: 15px;  /* 环形宽度(粗细) */
  --color: #7856d7;  /* 主色 */
  --inactive-color: #ccc;  /* 辅助色 */
}

然后利用 calc 将写死的数值改为根据 CSS 变量动态计算:

css 复制代码
/* 容器 */
.progress-circle {
  width: var(--size);
  height: var(--size);
  transform: rotate(-90deg);
  border-radius: 50%;
}

/* 进度条环形图形 */
.progress-circle > circle {
  cx: calc(var(--size) / 2);
  cy: calc(var(--size) / 2);
  r: calc((var(--size) - var(--border-width)) / 2);
  fill: none;
  stroke-width: var(--border-width);
  stroke-linecap: round;
  transition: stroke-dasharray 0.4s linear, stroke .3s;
}

SVG 中的 stroke-dasharray 也调整为根据 CSS 变量动态计算:

html 复制代码
<svg class="progress-circle">
  <circle stroke="var(--inactive-color)" />
  <circle stroke="var(--color)"
    style="stroke-dasharray: calc(
      2 * 3.1415 * (var(--size) - var(--border-width)) / 2 * 
      (var(--percent) / 100)
    ), 1000"
  />
</svg>

这样我们通过改变父容器的 --percent 变量就能直接控制进度条的百分比显示了。

同理,也可以改变其他内部变量来方便地控制组件的外观和尺寸。例如可以根据阈值来动态调整进度条颜色:

ts 复制代码
function changeProgress(percent) {
  progressEl.style.setProperty('--percent', percent);

  [
    { value: 90, color: '#7c5' },
    { value: 70, color: '#65c' },
    { value: 50, color: '#fc3' },
    { value: 0, color: '#f66' }
  ].find(it => {
    if (percent >= it.value) {
      progressEl.style.setProperty('--color', it.color);
      return true;
    }
  });
}

显示百分比文本

可以把百分比的文本显示在环形进度条的中央,增强可视化效果。有了前面的铺垫,这里只需用伪元素 + CSS 计数器就能轻松实现百分比文本显示。

由于 SVG 中不支持伪元素,所以我们加一层 HTML 标签作为主容器:

html 复制代码
<div class="progress-circle">
  <svg>
    ...
  </svg>
</div>

然后为主容器增加伪元素,居中定位,再利用 CSS 计数器接收 --percent 变量的值:

css 复制代码
/* 百分数文本 */
.progress-circle::before {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  counter-reset: progress var(--percent);
  content: counter(progress) '%';
  white-space: nowrap;
  font-size: 18px;
}

效果如下图:

完善边界场景

上面为圆环加了 stroke-linecap: round 让描边的端点圆角化,不过却引出了一个小问题:当真实进度为 0% 时,由于端点圆角的存在,使得进度条在视觉效果上明显大于0%,正如下图所示:

《纯CSS实现未读消息超过100自动显示为99+》一文的启发,我们为进度圆环加一个值为 --percentopacity 属性即可解决这个问题:

html 复制代码
<div class="progress-circle">
  <svg>
    <circle ... />
    <circle class="progress-value" ... />
  </svg>
</div>
css 复制代码
.progress-value {opacity: var(--percent);}

--percent 的值为 0 时,此处 opacity 为 0,则进度圆环完全透明不显示;当 --percent 的值大于 0 时,opacity 的值按 1 处理,则进度圆环正常显示。

扩展:仪表盘式进度条

环形进度条还有一种变体------仪表盘式进度条,顾名思义就是以仪表盘的形态显示进度,有着视觉吸引力强和易于理解的优点。下图是来自 AntDesign 的仪表盘式进度条示例:

在前面知识的基础上,我们趁热打铁再做一个仪表盘式进度条。

生成缺口

在视觉上,仪表盘式进度条相比圆环进度条最大的不同就是前者有个空白"缺口",且缺口朝下,整体左右对称。

我们很容易能够想到用 stroke-dasharray 的第1个参数来生成可见的圆弧(即"缺口"以外的部分),用第2个参数来生成间隙(即"缺口")。以中心夹角90°的缺口为例,代入弧长公式(l = πrα/180°)则有:

html 复制代码
<div class="progress-circle">
  <svg>
    <circle stroke="var(--inactive-color)"
            style="stroke-dasharray: calc(3.1415 * var(--r) * (360 - 90) / 180),
                                     calc(3.1415 * var(--r) * 90 / 180)"
    />
  </svg>
</div>

效果如下图所示:

确定 svg 旋转角度

在不对容器做任何旋转的情况下,这个90°缺口朝向右上角,而我们实际想要的是让缺口朝下,且整体左右对称。要达到这个目的,我们需要将 svg 容器顺时针旋转135°,效果如下:

在实际场景中,这个"缺口"的大小是可配置的,所以我们把缺口夹角封装到主容器的 CSS 变量里,方便后续的动态计算。

css 复制代码
.progress-circle {
  ...
  --gap-degree: 90;  /* 缺口夹角 */
}

当缺口夹角变成动态时,svg 容器到底需要旋转多少度才能使缺口的开口朝下,且整体左右对称呢?我们接着以90°夹角的缺口为例来分析。从前面实践可知想要让90°缺口朝下,且整体左右对称,则需要将 svg 容器顺时针旋转135°,旋转后的示意图如下:

其中 ∠A = ∠B = 135°,仔细观察可发现,这里的135°等于缺口自身夹角90°加上 ∠A 与 ∠B 重合的夹角45°. 显然,当缺口夹角发生变化时,此处的重合夹角需要动态计算,结合上图我们不难看出这里存在一个等式:

2 × 重合夹角 + 缺口夹角 = 180°

那么则有:

重合夹角 = (180° - 缺口夹角) / 2

结合 CSS 变量我们就可以确定 svg 容器的旋转角度,代码如下:

css 复制代码
.progress-circle > svg {
  ...
  transform: rotate(
    calc((var(--gap-degree) + (180 - var(--gap-degree)) / 2) * 1deg)
  );
}

这样我们的进度条就能适配不同的缺口夹角了:

修正进度换算

当我们按环形进度条的方式来测试仪表盘式进度条时,发现仪表盘式进度条的进度显示有问题------实际进度百分比和进度显示不匹配,如下图所示:

其实也不难理解,我们先看一下现有进度圆环的代码部分:

html 复制代码
<circle stroke="var(--color)"
  class="progress-value"
  style="stroke-dasharray: calc(2 * 3.1415 * var(--r) * (var(--percent) / 100)), 1000"
/>

在圆环进度条中,这里的 var(--percent) / 100α / 360° 是等价的,360°圆弧表示100%进度,但在仪表盘式进度条中,360°减去缺口夹角后对应的圆弧才真正表示100%进度。所以我们得把缺口夹角的因素换算进去,修正实际的进度显示。

我们从这个公式入手:l = 2πr * α/360°

在圆环进度条中,50%的进度对应180°的圆弧夹角,代入公式则有:

l = 2πr * 180 / 360

如换成仪表盘式进度条,当缺口夹角为90°时,这里的分母就是 360° - 90° = 270°。小学数学教会我们:分子分母等比变化时,其值不变,所以我们给分子也乘以分母变化的值,则有:

l = 2πr * 180 * (270 / 360) / 270

进一步分解下可得:l = πr * 180 * (270 / 180) / 270

我们把除缺口夹角外的圆弧换成 CSS 变量 --active-degree: calc(360 - var(--gap-degree));,写到进度圆环的代码中:

html 复制代码
<circle stroke="var(--color)"
  class="progress-value"
  style="stroke-dasharray: calc(3.1415 * var(--r) * 180 * var(--active-degree) / 180 / var(--active-degree)), 1000"
/>

其中 (180 * var(--active-degree) / 180) / var(--active-degree) 等价于 (var(--percent) * var(--active-degree) / 180) / 100,答案已经呼之欲出了:

html 复制代码
<circle stroke="var(--color)"
  class="progress-value"
  style="stroke-dasharray: calc(3.1415 * var(--r) * var(--percent) * var(--active-degree) / 180 / 100), 1000"
/>

最终效果:(Demo 传送门)

最后

利用 CSS 和 SVG 技术,我们成功地实现了一个优雅的环形进度条,这个进度条不仅能够展示进度,还能够通过 CSS 变量来调整样式满足不同的需求。希望这篇分享对大家使用 CSS 和 SVG 有所帮助,也希望大家能够在实践中不断探索和创新,做出更加实用和美观的作品。

关于OpenTiny

OpenTiny 是一套企业级 Web 前端开发解决方案,提供跨端、跨框架的UI组件库,适配 PC 端 / 移动端等多端,支持 Vue2 / Vue3 / Angular 多技术栈,拥有灵活扩展的低代码引擎,包含主题配置系统 / 中后台模板 / CLI 命令行等丰富的效率提升工具,可帮助开发者高效开发 Web 应用。

核心亮点:

  • 跨端跨框架: 使用 Renderless 无渲染组件设计架构,实现了一套代码同时支持 Vue2 / Vue3,PC / Mobile 端,并支持函数级别的逻辑定制和全模板替换,灵活性好、二次开发能力强。

  • 组件丰富:PC 端有100+组件,移动端有30+组件,包含高频组件 Table、Tree、Select 等,内置虚拟滚动,保证大数据场景下的流畅体验,除了业界常见组件之外,我们还提供了一些独有的特色组件,如:Split 面板分割器、IpAddress IP 地址输入框、Calendar 日历、Crop 图片裁切等。

  • 低代码引擎:低代码引擎使能开发者定制低代码平台。它是低代码平台的底座,提供可视化搭建页面等基础能力,既可以通过线上搭配组合,也可以通过下载源码进行二次开发,实时定制出自己的低代码平台。适用于多场景的低代码平台开发,如:资源编排、服务端渲染、模型驱动、移动端、大屏端、页面编排等。

  • 配置式组件: 组件支持模板式和配置式两种使用方式,适合低代码平台,目前团队已经将 OpenTiny 集成到内部的低代码平台,针对低码平台做了大量优化。

  • 周边生态齐全: 提供了基于 Angular + TypeScript 的 TinyNG 组件库,提供包含 10+ 实用功能、20+ 典型页面的 TinyPro 中后台模板,提供覆盖前端开发全流程的 TinyCLI 工程化工具,提供强大的在线主题配置平台 TinyTheme。


欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~

OpenTiny 官网:opentiny.design/

OpenTiny 代码仓库:github.com/opentiny/

TinyEngine 源码: github.com/opentiny/ti...

欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~

如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

往期文章推荐

相关推荐
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596933 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai3 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书