环形进度条虽然很常用,但基本都是直接用各种组件库来搞,所以在此之前也没有自己真正实现过。偶然思考了一下这个问题,直接用CSS的话,好像还不太好实现,所以看了下svg实现的方案,也趁机熟悉一下svg——svg实现环形进度环出乎意料的简单。
一、效果预览
可在线体验效果:https://wintc.top/laboratory/#/circle-progress。
该效果代码大概如下:
<div class="progress-container progress" style="width: 100px; height: 100px;">
<svg viewBox="0 0 100 100">
<path d="M 50 2 A 48 48 0 0 1 98 50 A 48 48 0 1 1 50 2" fill="none" stroke-width="4" stroke="#d5d5d6"></path>
<path
d="M 50 2 A 48 48 0 0 1 95.55661495953414 34.880647056545364"
fill="none"
stroke-width="4"
stroke-linecap="round"
stroke="red"
class="path"
style="stroke-dasharray: 60.3186, 241.274;">
</path>
</svg>
<div class="inner-content">20%</div>
</div>
可以看到,其中圆环图形部分对应一个svg元素,svg元素内有两个path标签,分别画了灰色背景圆环和红色进度环。接下来简单介绍一下这几个元素内的几个重要属性。
二、SVG坐标
svg的坐标系是一个直角坐标系,不过与初高中数学中的坐标系y轴正方向相反:
通过坐标系,可以约束svg图形从哪里开始画,画一个什么形状,到哪里结束等等。默认情况下,1个坐标单位和1屏幕像素对应。为了让我们实现的进度条组件复用性更高,我们可以在根级svg元素设置viewBox属性,用于约束缩放。比如:
<svg viewBox="0 0 100 100"></svg>
这样不管svg实际宽度对应多少像素,其内部可见的坐标点x、y坐标都在0~100之间。
三、<path>元素
path元素有一个最重要的属性就是d属性,d属性的值可以是一连串的指令拼接而成。一个指令由指令字母+参数组成。已上述例子中的第一个path为例:
<path d="M 50 2 A 48 48 0 0 1 98 50 A 48 48 0 1 1 50 2" fill="none" stroke-width="4" stroke="#d5d5d6"></path>
d属性中用到了两种指令,M指令和A指令,完整的指令列表为:
-
- M = moveto
- L = lineto
- H = horizontal lineto
- V = vertical lineto
- C = curveto
- S = smooth curveto
- Q = quadratic Belzier curve
- T = smooth quadratic Belzier curveto
- A = elliptical Arc
- Z = closepath
以上所有命令均允许小写字母。大写表示绝对定位,小写表示相对定位。
这里仅仅介绍M指令和A指令,因为仅用这两种指令就可以完成进度环的绘制。
M指令很简单,后面跟随的两个数字x,y作为参数,表示移动画笔到x,y坐标,这时候还没有开始画线;如果使用小写字母m后面跟随数字dx, dy,则表示画笔从当前位置水平、垂直方向各移动d、dy个坐标(相对当前位置而不是原点进行移动)。
A指令稍微复杂,因为参数实在太多了。A指令作用是画椭圆弧形,指令格式为:
A rx ry x-axis-rotation large-arc-flag sweep-flag x y。
椭圆有长轴和短轴两个轴线,在A指令里通过前两个参数rx、ry来表示,这里我们为了画个圆,所以rx、ry设置为一致即可。试想,通过M指令规定了起点,通过rx、ry规定了弧线的长短轴,很显然我们还需要其它约束条件来得到一条圆弧。A指令的最后两个参数x、y约束了椭圆弧线的终点。给定起点A、终点B、长短轴rx/ry,可以得到4条弧线(如下图)。
要想确定下图中的某一条弧线,还需要另外两个参数large-arc-flag和sweep-flag,两个参数的可选值都是0和1。sweep-flag表示起点到终点是顺时针还是逆时针,下图中前两种情况即为顺时针(sweep-flag设置为1),后两种情况为逆时针(sweep-flag设置为0)。对于顺时针的两条弧线,large-arc-flag控制取长弧还是短弧,1为长弧,0为短弧。
A指令还有一个参数是x-axis-rotation,可以用于控制弧线旋转的角度。
通过path元素,我们已经可以刻画任意角度的圆弧,配上一个完整的背景圆弧环,就可以完成0 ~ 100%任意进度的进度环了。
四、0 ~ 100%任意进度进度环
任意进度的进度环,假设起点固定为圆环最顶部的点,无非是根据进度计算上述path中的各个参数的值。根据角度和圆弧半径计算终点坐标,就是高中所学的“极坐标转换为直角坐标”,利用三角函数直接转换即可。而使用长弧还是短弧,我们只用判断进度是否大于一半即可。原理很简单,Vue组件代码如下:
<template>
<div
:style="{
width: size,
height: size
}"
class="progress-container">
<svg :viewBox="`0 0 ${svgWidth} ${svgWidth}`">
<path
:d="backPath"
fill="none"
:stroke-width="lineWidth"
stroke="#d5d5d6">
</path>
<path
class="path"
:d="path"
fill="none"
:stroke-width="lineWidth"
stroke-linecap="round"
:style="{
'stroke-dasharray': dashArray,
}"
stroke="red">
</path>
</svg>
<div
class="inner-content">
<slot>
{{ progressText }}
</slot>
</div>
</div>
</template>
<script>
const LINE_WIDTH = 4
const SVG_WIDTH = 100
export default {
props: {
progress: {
type: Number,
default: 50
},
size: {
type: String,
default: '100px'
}
},
computed: {
lineWidth () {
return LINE_WIDTH
},
svgWidth () {
return SVG_WIDTH
},
radius () {
return (this.svgWidth - this.lineWidth) / 2
},
fixedProgress () {
return Math.max(Math.min(100, this.progress), 0)
},
progressText () {
return this.fixedProgress + '%'
},
deg () {
return 2 * Math.PI * (this.fixedProgress - 0.1) / 100
},
backPath () {
let sx = this.svgWidth / 2, sy = this.lineWidth / 2
let dx = this.svgWidth - this.lineWidth / 2, dy = this.svgWidth / 2
let r = this.radius
return `M ${sx} ${sy} A ${r} ${r} 0 0 1 ${dx} ${dy} A ${r} ${r} 0 1 1 ${sx} ${sy}`
},
path () {
let r = this.radius
let sx = this.svgWidth / 2, sy = this.lineWidth / 2
let dx = this.svgWidth / 2 + Math.sin(this.deg) * r
let dy = this.svgWidth / 2 - Math.cos(this.deg) * r
let arc = this.fixedProgress > 50 ? 1 : 0
return `M ${sx} ${sy} A ${r} ${r} 0 ${arc} 1 ${dx} ${dy}`
},
dashArray () {
let ratio = this.fixedProgress / 100
let c = Math.PI * 2 * this.radius
return `${c * ratio / this.svgWidth * 100}, ${c * (1 - ratio) / this.svgWidth * 100}`
}
}
}
</script>
<style lang="stylus" scoped>
.progress-container
position relative
.inner-content
position absolute
left 5%
right 5%
top 5%
bottom 5%
z-index 2
display flex
align-items center
justify-content center
.path
transition stroke-dasharray .4s ease
</style>
进度变化的时候,如果直接改变可能会表现得很生硬,这里使用了stroke-dasharray这个CSS属性做过渡效果,这样在进度增加的时候,有一个动画增加的效果。这个CSS属性用一个偶数个数字(如果数字为奇数个,则重复一遍变为偶数)来描述线条的虚实相间效果,第一个数字表示线条实线长度,第二个数字表示空白线条长度,依次类推循环。
至此,一个圆形svg进度环⭕️就完成了。