其中進(jìn)度、尺寸庭惜、環(huán)寬和顏色都可以非常方便地進(jìn)行控制罩驻。
核心原理:
利用兩個(gè)重疊的圓環(huán)形,通過對上層圓環(huán)弧長的控制來表示進(jìn)度护赊,下層圓環(huán)則作為輔助惠遏,呈現(xiàn)環(huán)形進(jìn)度條剩余的部分砾跃。
核心知識點(diǎn):
SVG circlestroke-dasharray
弧長公式l = πrα/180°
CSS 變量
CSS 計(jì)數(shù)器
下面分享下具體實(shí)現(xiàn)過程。
實(shí)現(xiàn)環(huán)形
要實(shí)現(xiàn)環(huán)形节吮,有多種技術(shù)可供選擇抽高,包括 Canvas、SVG透绩,甚至 CSS + HTML 的組合翘骂。在本文中,我們使用了 SVG 方案帚豪,一來是因?yàn)?SVG 的 API 豐富碳竟,圖形表現(xiàn)力非常強(qiáng)大,二來是 SVG 可以與 CSS 無縫搭配使用狸臣,實(shí)現(xiàn)更加強(qiáng)大的功能莹桅。
用 SVG 中的 circle 標(biāo)簽可以毫無壓力的繪出一個(gè)直徑 200px 的圓環(huán):
```
<svg width="400" height="400">
? <circle
? ? cx="200"
? ? cy="200"
? ? r="100"
? ? fill="none"
? ? stroke="blue"
? ? stroke-width="10" />
</svg>
```
我們再調(diào)整下代碼,讓圓環(huán)圖形完美適應(yīng) SVG 容器的 200×200 大泄膛铩:
```
<svg width="200" height="200">
? <circle
? ? cx="100"
? ? cy="100"
? ? r="95"
? ? fill="none"
? ? stroke="blue"
? ? stroke-width="10" />
</svg>
```
小技巧:為了完美適應(yīng)容器尺寸统翩,我們可以將半徑 r 的值設(shè)置為容器寬度的一半減去 stroke-width 大小的一半,這樣做可以確保圓環(huán)不會因?yàn)橐绯龆蝗萜骷舨玫舸酥蕖4颂?r = 200 / 2 - 10 / 2厂汗,即 (200 - 10) / 2 = 95
接著使用相同的方法繪制另一個(gè)圓環(huán),作為輔助圓環(huán)呜师。為了視覺效果娶桦,進(jìn)度圓環(huán)應(yīng)該在上層,因此在代碼中汁汗,進(jìn)度圓環(huán)的標(biāo)簽應(yīng)該放在輔助圓環(huán)的后面衷畦。完成后的代碼如下:
```
<svg width="200" height="200">
? <!-- 輔助圓環(huán) -->
? <circle
? ? cx="100"
? ? cy="100"
? ? r="95"
? ? fill="none"
? ? stroke="#ccc"
? ? stroke-width="10" />
? <!-- 進(jìn)度圓環(huán) -->
? <circle
? ? cx="100"
? ? cy="100"
? ? r="95"
? ? fill="none"
? ? stroke="blue"
? ? stroke-width="10" />
</svg>
```
可以發(fā)現(xiàn)代碼中有很多重復(fù)的屬性,為了讓代碼更加簡潔和高效知牌,我們可以使用 CSS 將這些重復(fù)的部分提取出來祈争,統(tǒng)一聲明,讓代碼變得更加“干”(DRY):
```
.progress-circle {
? width: 200px;
? height: 200px;
}
.progress-circle > circle {
? cx: 100px;
? cy: 100px;
? r: 95px;
? fill: none;
? stroke-width: 10px;
}
```
```
<svg class="progress-circle">
? <circle stroke="#ccc" />
? <circle stroke="blue" />
</svg>
```
實(shí)現(xiàn)圓環(huán)進(jìn)度
上下兩層的圓環(huán)已經(jīng)準(zhǔn)備好了角寸,現(xiàn)在的重點(diǎn)是如何實(shí)現(xiàn)上層圓環(huán)上的進(jìn)度菩混,這個(gè)問題可以分為 2 個(gè)關(guān)鍵點(diǎn):
1.如何實(shí)現(xiàn)圓環(huán)的弧長?
2.如何將進(jìn)度百分比轉(zhuǎn)換為圓環(huán)的弧長扁藕?
要解決第 1 個(gè)問題沮峡,這里要用到 SVG 中的stroke-dasharray屬性,這是一個(gè)用來控制路徑虛線疏密程度的屬性亿柑,其值是一組描述虛線的短劃線與空白間隙長度的數(shù)列邢疙。例如,如果設(shè)置stroke-dasharray="5 2",則路徑將以 5 個(gè)像素的短劃線和 2 個(gè)像素的空白間隙交替顯示疟游,其中第一個(gè)數(shù)控制短劃線長度呼畸,第二個(gè)數(shù)控制空白間隙長度。
stroke-dasharray 的參數(shù)值還支持多個(gè)數(shù)列颁虐,詳情見 MDN 文檔
很明顯役耕,這里我們需要控制圓弧的長度(即虛線中的短劃線長度)來呈現(xiàn)進(jìn)度。然而聪廉,由于虛線中的短劃線是多個(gè)且重復(fù)的瞬痘,僅僅改變短劃線長度并不能滿足我們的需求,具體情況如下圖所示:
根據(jù)需求板熊,我們只需要圓環(huán)的一段圓弧即可框全,那如何實(shí)現(xiàn)呢?經(jīng)過多次嘗試干签,我們發(fā)現(xiàn)當(dāng)改變虛線中空白間隙的長度(即stroke-dasharray的第二個(gè)數(shù))津辩,當(dāng)這個(gè)長度超過圓環(huán)的周長時(shí),視覺效果上圓環(huán)只剩下一條獨(dú)立的圓弧了容劳,此時(shí)我們可以通過調(diào)整第一個(gè)數(shù)來改變圓弧的長度喘沿,從而解決了上面第 1 個(gè)問題。
上面第 2 個(gè)問題暫時(shí)看起來比較棘手竭贩,因?yàn)槲覀兒茈y看出 0%~100%之間的進(jìn)度到底對應(yīng)多長的弧長蚜印。我們繼續(xù)探究,尋找規(guī)律留量。
根據(jù)需求窄赋,當(dāng)進(jìn)度百分比是 100%時(shí),進(jìn)度條的圓弧要呈現(xiàn)出一個(gè)圓環(huán)楼熄,此時(shí)圓弧夾角是 360°忆绰,當(dāng)進(jìn)度百分比是 50%時(shí),圓弧是個(gè)半圓環(huán)可岂,夾角是 180° 错敢。反過來也成立:180°表示 50%,360°表示 100% 缕粹≈擅可以看出進(jìn)度百分比和角度是存在等量關(guān)系的,同時(shí)根據(jù)弧長公式l = πrα/180°致开,帶入角度就可以求出弧長了峰锁,至此“進(jìn)度百分比 - 角度 - 弧長”三者的規(guī)律就清晰了萎馅,思路馬上要通了双戳。
l = πrα/180°
其中 l 表示弧長,π 是圓周率糜芳,r 表示半徑飒货,α 表示夾角
在實(shí)際使用中魄衅,我們是用百分?jǐn)?shù)來控制弧長的,而不是用角度塘辅,所以接下來把進(jìn)度百分比和弧長關(guān)聯(lián)起來晃虫。
我們把弧長公式變動下,分子分母同時(shí)乘以 2扣墩,則有:
l = 2πrα/180°*2哲银,即l = 2πr * α/360°。
同時(shí)根據(jù)前面可知進(jìn)度百分比和角度存在等量關(guān)系(360° 等于 100%)呻惕,所以可以得到:
l = 2πr * p/100荆责,其中p為當(dāng)前進(jìn)度百分?jǐn)?shù)。
現(xiàn)在亚脆,我們就可以根據(jù)進(jìn)度百分比來計(jì)算出對應(yīng)的弧長了做院。
優(yōu)化細(xì)節(jié)
環(huán)形進(jìn)度條通常都是從 12 點(diǎn)鐘方向開始的,而 SVG 中默認(rèn)是 3 點(diǎn)鐘方向作為起點(diǎn)濒持,所以我們給它偏轉(zhuǎn)-90°進(jìn)行修正:
```
.progress-circle {
? ...
? transform: rotate(-90deg);
}
```
另外我們發(fā)現(xiàn)圓弧的端點(diǎn)處過于生硬键耕,給個(gè)圓角效果修飾下是個(gè)好主意。這里我們用了 SVG 中的屬性stroke-linecap:
```
.progress-circle > circle {
? ...
? stroke-linecap: round;
}
```
當(dāng)然了柑营,動畫過渡效果也可以安排上屈雄,讓進(jìn)度條的變化更加絲滑:
```
.progress-circle > circle {
? ...
? transition: stroke-dasharray 0.4s linear, stroke .3s;
}
```
組件化
環(huán)形進(jìn)度條的效果雖然已經(jīng)實(shí)現(xiàn)了,但光靠上面的那些代碼還是很難復(fù)用官套。環(huán)形進(jìn)度條的寬度棚亩、高度、半徑虏杰、顏色等都是寫死的讥蟆,另外stroke-dasharray的值還得靠 JS 進(jìn)行計(jì)算,再賦值給<circle>元素纺阔。說好的純 CSS 實(shí)現(xiàn)呢瘸彤?
要解決這些問題,我們需要將環(huán)形進(jìn)度條組件化笛钝。
先定義一些組件全局要用到的 CSS 變量:
```
/* 容器 */
.progress-circle {
? --percent: 0;? /* 百分?jǐn)?shù) */
? --size: 180px;? /* 尺寸大小 */
? --border-width: 15px;? /* 環(huán)形寬度(粗細(xì)) */
? --color: #7856d7;? /* 主色 */
? --inactive-color: #ccc;? /* 輔助色 */
}
```
然后利用calc將寫死的數(shù)值改為根據(jù) CSS 變量動態(tài)計(jì)算:
```
/* 容器 */
.progress-circle {
? width: var(--size);
? height: var(--size);
? transform: rotate(-90deg);
? border-radius: 50%;
}
/* 進(jìn)度條環(huán)形圖形 */
.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也調(diào)整為根據(jù) CSS 變量動態(tài)計(jì)算:
```
<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變量就能直接控制進(jìn)度條的百分比顯示了质况。
同理,也可以改變其他內(nèi)部變量來方便地控制組件的外觀和尺寸玻靡。例如可以根據(jù)閾值來動態(tài)調(diào)整進(jìn)度條顏色:
```
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;
? ? }
? });
}
```
顯示百分比文本
可以把百分比的文本顯示在環(huán)形進(jìn)度條的中央结榄,增強(qiáng)可視化效果。有了前面的鋪墊囤捻,這里只需用偽元素 + CSS 計(jì)數(shù)器就能輕松實(shí)現(xiàn)百分比文本顯示臼朗。
由于 SVG 中不支持偽元素,所以我們加一層 HTML 標(biāo)簽作為主容器:
```
<div class="progress-circle">
? <svg>
? ? ...
? </svg>
</div>
```
然后為主容器增加偽元素,居中定位视哑,再利用 CSS 計(jì)數(shù)器接收--percent變量的值:
```
/* 百分?jǐn)?shù)文本 */
.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;
}
```
效果如下圖:
完善邊界場景
上面為圓環(huán)加了stroke-linecap: round讓描邊的端點(diǎn)圓角化绣否,不過卻引出了一個(gè)小問題:當(dāng)真實(shí)進(jìn)度為 0% 時(shí),由于端點(diǎn)圓角的存在挡毅,使得進(jìn)度條在視覺效果上明顯大于 0%蒜撮,正如下圖所示:
受《純CSS實(shí)現(xiàn)未讀消息超過100自動顯示為99+》一文的啟發(fā),我們?yōu)檫M(jìn)度圓環(huán)加一個(gè)值為 --percent 的 opacity 屬性即可解決這個(gè)問題:
```
<div class="progress-circle">
? <svg>
? ? <circle ... />
? ? <circle class="progress-value" ... />
? </svg>
</div>
```
```
.progress-value {opacity: var(--percent);}
```
當(dāng) --percent 的值為 0 時(shí)跪呈,此處 opacity 為 0段磨,則進(jìn)度圓環(huán)完全透明不顯示;當(dāng) --percent 的值大于 0 時(shí)耗绿,opacity 的值按 1 處理薇溃,則進(jìn)度圓環(huán)正常顯示。
擴(kuò)展:儀表盤式進(jìn)度條
環(huán)形進(jìn)度條還有一種變體——儀表盤式進(jìn)度條缭乘,顧名思義就是以儀表盤的形態(tài)顯示進(jìn)度沐序,有著視覺吸引力強(qiáng)和易于理解的優(yōu)點(diǎn)。下圖是來自AntDesign的儀表盤式進(jìn)度條示例:
在前面知識的基礎(chǔ)上堕绩,我們趁熱打鐵再做一個(gè)儀表盤式進(jìn)度條策幼。
生成缺口
在視覺上,儀表盤式進(jìn)度條相比圓環(huán)進(jìn)度條最大的不同就是前者有個(gè)空白“缺口”奴紧,且缺口朝下特姐,整體左右對稱。
我們很容易能夠想到用stroke-dasharray的第 1 個(gè)參數(shù)來生成可見的圓皇虻(即“缺口”以外的部分)唐含,用第 2 個(gè)參數(shù)來生成間隙(即“缺口”)。以中心夾角 90°的缺口為例沫浆,代入弧長公式(l = πrα/180°)則有:
```
<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旋轉(zhuǎn)角度
在不對容器做任何旋轉(zhuǎn)的情況下捷枯,這個(gè) 90°缺口朝向右上角,而我們實(shí)際想要的是讓缺口朝下专执,且整體左右對稱淮捆。要達(dá)到這個(gè)目的,我們需要將svg容器順時(shí)針旋轉(zhuǎn) 135°本股,效果如下:
在實(shí)際場景中攀痊,這個(gè)“缺口”的大小是可配置的,所以我們把缺口夾角封裝到主容器的 CSS 變量里拄显,方便后續(xù)的動態(tài)計(jì)算苟径。
```
.progress-circle {
? ...
? --gap-degree: 90;? /* 缺口夾角 */
}
```
當(dāng)缺口夾角變成動態(tài)時(shí),svg容器到底需要旋轉(zhuǎn)多少度才能使缺口的開口朝下躬审,且整體左右對稱呢棘街?我們接著以 90°夾角的缺口為例來分析蟆盐。從前面實(shí)踐可知想要讓 90°缺口朝下,且整體左右對稱蹬碧,則需要將svg容器順時(shí)針旋轉(zhuǎn) 135°,旋轉(zhuǎn)后的示意圖如下:
其中∠A = ∠B = 135°炒刁,仔細(xì)觀察可發(fā)現(xiàn)恩沽,這里的 135°等于缺口自身夾角 90°加上 ∠A 與 ∠B 重合的夾角 45°. 顯然,當(dāng)缺口夾角發(fā)生變化時(shí)翔始,此處的重合夾角需要?jiǎng)討B(tài)計(jì)算罗心,結(jié)合上圖我們不難看出這里存在一個(gè)等式:
2 × 重合夾角 + 缺口夾角 = 180°
那么則有:
重合夾角 = (180° - 缺口夾角) / 2
結(jié)合 CSS 變量我們就可以確定svg容器的旋轉(zhuǎn)角度,代碼如下:
```
.progress-circle > svg {
? ...
? transform: rotate(
? ? calc((var(--gap-degree) + (180 - var(--gap-degree)) / 2) * 1deg)
? );
}
```
這樣我們的進(jìn)度條就能適配不同的缺口夾角了:
修正進(jìn)度換算
當(dāng)我們按環(huán)形進(jìn)度條的方式來測試儀表盤式進(jìn)度條時(shí)城瞎,發(fā)現(xiàn)儀表盤式進(jìn)度條的進(jìn)度顯示有問題——實(shí)際進(jìn)度百分比和進(jìn)度顯示不匹配渤闷,如下圖所示:
其實(shí)也不難理解,我們先看一下現(xiàn)有進(jìn)度圓環(huán)的代碼部分:
```
<circle stroke="var(--color)"
? class="progress-value"
? style="stroke-dasharray: calc(2 * 3.1415 * var(--r) * (var(--percent) / 100)), 1000"
/>
```
在圓環(huán)進(jìn)度條中脖镀,這里的var(--percent) / 100與α / 360°是等價(jià)的飒箭,360°圓弧表示 100%進(jìn)度,但在儀表盤式進(jìn)度條中蜒灰,360°減去缺口夾角后對應(yīng)的圓弧才真正表示 100%進(jìn)度弦蹂。所以我們得把缺口夾角的因素?fù)Q算進(jìn)去,修正實(shí)際的進(jìn)度顯示强窖。
我們從這個(gè)公式入手:l = 2πr * α/360°
在圓環(huán)進(jìn)度條中凸椿,50%的進(jìn)度對應(yīng) 180°的圓弧夾角,代入公式則有:
l = 2πr * 180 / 360
如換成儀表盤式進(jìn)度條翅溺,當(dāng)缺口夾角為 90°時(shí)脑漫,這里的分母就是360° - 90° = 270°。小學(xué)數(shù)學(xué)教會我們:分子分母等比變化時(shí)咙崎,其值不變优幸,所以我們給分子也乘以分母變化的值,則有:
l = 2πr * 180 * (270 / 360) / 270
進(jìn)一步分解下可得:l = πr * 180 * (270 / 180) / 270
我們把除缺口夾角外的圓弧換成 CSS 變量--active-degree: calc(360 - var(--gap-degree));褪猛,寫到進(jìn)度圓環(huán)的代碼中:
```
<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)等價(jià)于(var(--percent) * var(--active-degree) / 180) / 100劈伴,答案已經(jīng)呼之欲出了:
```
<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 技術(shù),我們成功地實(shí)現(xiàn)了一個(gè)優(yōu)雅的環(huán)形進(jìn)度條握爷,這個(gè)進(jìn)度條不僅能夠展示進(jìn)度跛璧,還能夠通過 CSS 變量來調(diào)整樣式滿足不同的需求。希望這篇分享對大家使用 CSS 和 SVG 有所幫助新啼,也希望大家能夠在實(shí)踐中不斷探索和創(chuàng)新追城,做出更加實(shí)用和美觀的作品。