利用svg實現帶加載進度的loading

什麼是svg

svg是基於XML,由World Wide Web Consortium (W3C)聯盟開發的一種開放標準的矢量圖形語言,可讓你設計激動人心的、高分辨率的Web圖形頁面。

可能有人不知道XML是什麼,你隻需知道他和html一樣也是一種頁面結構。他們同樣都是由W3C開發的,隻不過xml用來設計web圖形,html用來構建web頁面。

loading就屬於web圖形的一種,所以是可以使用基於XML的svg來實現的。同時因為他和html是同一級別的,所以兼容性不必多說。

用到svg元素、屬性介紹

circle標簽

circle是svg中的一個基礎標簽,類似於html的div。不同的是div是長方形,circle是圓形。

他有三個專有屬性也是基本參數:cxcyr。前兩個表示圓心的水平、垂直坐標,r表示圓的半徑。

<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <circle cx="50" cy="50" r="50"/>
</svg>

text標簽

text元素定義瞭一個由文字組成的圖形,可以理解為專門用來裝文字的div。

tspan標簽

在text元素中,利用內含的tspan元素,可以調整文本和字體的屬性以及當前文本的位置、絕對或相對坐標值。

stroke-dasharray屬性

stroke-dasharray屬性是實現loading最關鍵的一個屬性。

  • 這個屬性的作用是為他所在的元素畫一條線,這條線的位置相當於是border的概念。
  • 與border不同的是,他所傳入的值隻用來表示線的類型類似於border-style,沒有顏色和寬度。
  • 它用來表示線型的方法不是采用 solid dashed 等這樣的名稱來命名,而是通過一段有間隔的數字來分別表示線的長度和間隔長度。
<svg width="200" height="200" viewPort="0 0 200 300" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <line stroke-dasharray="5, 5"              x1="10" y1="10" x2="190" y2="10" />
    <line stroke-dasharray="5, 10"             x1="10" y1="30" x2="190" y2="30" />
    <line stroke-dasharray="10, 5"             x1="10" y1="50" x2="190" y2="50" />
    <line stroke-dasharray="5, 1"              x1="10" y1="70" x2="190" y2="70" />
    <line stroke-dasharray="1, 5"              x1="10" y1="90" x2="190" y2="90" />
    <line stroke-dasharray="0.9"               x1="10" y1="110" x2="190" y2="110" />
    <line stroke-dasharray="15, 10, 5"         x1="10" y1="130" x2="190" y2="130" />
    <line stroke-dasharray="15, 10, 5, 10"     x1="10" y1="150" x2="190" y2="150" />
    <line stroke-dasharray="15, 10, 5, 10, 15" x1="10" y1="170" x2="190" y2="170" />
    <line stroke-dasharray="5, 5, 1, 5"        x1="10" y1="190" x2="190" y2="190" />

    <style><![CDATA[
    line{
        stroke: black;
        stroke-width: 2;
    }
    ]]></style>
</svg>

可以看出第一行線的長度和間隔長度都是5。第二行線長是5間隔長度是10,因此空白是實線的2倍。第三行線長是10間隔是5,因此實心是空白的2倍。後面的以此類推。

stroke-linecap屬性

stroke-linecap表示線頭的類型。這是svg獨有的特性,他有三個常見屬性如下:

<svg viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
  <line x1="1" y1="1" x2="5" y2="1" stroke="black" stroke-linecap="butt" />
  <line x1="1" y1="3" x2="5" y2="3" stroke="black" stroke-linecap="round" />
  <line x1="1" y1="5" x2="5" y2="5" stroke="black" stroke-linecap="square" />
    <!--線的真實長度-->
  <path d="M1,1 h4 M1,3 h4 M1,5 h4" stroke="pink" stroke-width="0.025" />
</svg>

這裡需要特別註意的是其中粉色線的長度才是真實線的長度,當stroke-linecap的值為round或square時,視覺上會使線的頭尾加長。

其他屬性

  • stroke-width: 表示線的寬度,類似於border-width
  • stroke: 表示線的顏色,類似於border-color
  • fill 表示填充色,類似於background-color,不同的是隻要是svg的元素就可以使用他填充顏色,包括文字標簽。當不設置時默認值為黑色,設置為none時為透明。

circle標簽添加stroke-width的效果

<svg width="100" height="100">
    <circle 
        cx="50" cy="50" r="40" 
        stroke-width="10" stroke="red" fill="green" stroke-dasharray="50 252"
    ></circle>
</svg>

這裡註意默認情況下是從圓的最右邊順時針開始畫線,而不是最上邊

實現loading

實現原理

通過動態控制進度圓stroke-dasharray屬性的第一個參數大小,來表示loading進度。

這麼說可能很難理解,看完實現步驟再回來看就很好理解瞭。

實現步驟

1.在svg標簽中創建兩個圓,為他們設置相同的圓心(cx、cy)、半徑r、邊框寬度stroke-width。滿足 cx + r + stroke-width = svg的寬。

2.為兩個圓分別設置他們的邊框顏色stroke,這裡需要註意的是因為兩個圓圓心、半徑相同所以第二個圓會覆蓋到第一個圓上面。

我們將覆蓋在上面的圓(代碼中後寫的circle)叫做進度圓,他的顏色設置為進度條的顏色。

將被蓋住的圓(代碼中第一個寫的circle)叫做背景圓,他的顏色設置為圓環的背景色。

此時頁面應該顯示的是100%進度的loading圓環,如下:

<svg width="100" height="100">
    <!--背景圓-->
    <circle cx="50" cy="50" r="40" stroke-width="10" stroke="#eee" fill="none"></circle>
    <!--進度圓-->
    <circle cx="50" cy="50" r="40" stroke-width="10" stroke="red" fill="none"
    ></circle>
</svg>

3.添加進度文字,需要在circle下方添加一個text文本框,在其中放入兩個tspan元素。一個用來更新進度,一個放%。

<svg width="100" height="100">
    <circle cx="50" cy="50" r="40" stroke-width="10" stroke="#eee" fill="none"></circle>
    <circle cx="50" cy="50" r="40" stroke-width="10" stroke="red" fill="none"></circle>
    <text x="50" y="56" text-anchor="middle">
        <tspan>100</tspan><tspan>%</tspan>
    </text>
</svg>

這裡需要註意的是,text標簽的y屬性設置成50會默認偏上,經過我的測試加上其字體大小的三分之一效果剛剛好。

4.使用計時器模擬加載進度並且動態更新進度圓stroke-dasharray屬性的第一個參數大小 。這裡采用react來模擬計時器。

// app.js
import {useState, useEffect} from 'react';
import Loading from "./loading";

function App() {
  const [percent, setPrecent] = useState(0);
    useEffect(() => {
      let timer = setInterval(() => {
        setPrecent(percent => {
          percent+= 5;
          if(percent === 100) {
            clearInterval(timer);
          }
          return percent;
        });
      }, 200)
      return () => clearInterval(timer);
    }, [])
  
  return (
    <div >
      <Loading percent={percent} />
    </div>
  );
}
export default App;
// loading/index.jsx
import styles from './style.module.scss';
export default function Loading({
    size = 100,
    r = 40,
    strokeWidth = 10,
    stroke = 'red',
    strokeBg = '#ccc',
    strokeLinecap = 'butt',
    percent = 0,
    fontSize = '16',
    fontColor = '#6b778c'
}) {
    let perimeter = 2 * Math.PI * r;
    let strokeDasharray = `${Math.floor(percent / 100 * perimeter)} ${perimeter}`;

    return (
        <svg className={styles.svg} width={size} height={size}>
            // 背景圓
            <circle 
                cx={size / 2} cy={size / 2} r={r} strokeWidth={strokeWidth} stroke={strokeBg}
            ></circle>
            // 進度圓
            <circle
                className={styles.show}
                cx={size / 2} cy={size / 2} r={r} strokeWidth={strokeWidth} stroke={stroke}
                strokeDasharray={strokeDasharray} strokeLinecap={strokeLinecap}
            ></circle>
            <text x={size / 2} y={size / 2 + fontSize / 3} fill={fontColor} fontSize={fontSize} textAnchor="middle">
                <tspan>{percent}</tspan><tspan>%</tspan>
            </text>
        </svg>
    );
}

註意樣式中要加上逆時針旋轉90度,因為在圓上畫線時默認從圓的最右邊開始順時針畫。而我們的要求是從最上方開始順時針畫,所以要逆時針旋轉90deg。

// style.module.scss
@keyframes rotate {
    0% {
        transform: rotate(-90deg); 
    }
    25% {
        transform: rotate(0deg);
    }
    50% {
        transform: rotate(90deg); 
    }
    75% {
        transform: rotate(180deg);
    }
    100% {
        transform: rotate(270deg);
    }

}
svg {
    circle {
        fill: none;
    }
    .show {
        // 逆時針旋轉90deg
        transform: rotate(-90deg);
        transform-origin: center;
        transition: all 1s;
        animation: rotate 1s linear infinite;
    }
}

此時效果如圖,已經達到瞭我們想要的效果。

loding優化

采用默認的線頭(butt)時,邊緣很尖給人一種很尖銳的感覺,可以通過給strokeLinecap傳入round屬性將線頭變為圓形,這樣就看上去更加流暢瞭。

strokeLinecap等於round時的bug

上面提到將線頭strokeLinecap優化為round,但是優化完會產生兩個不容易被發現的bug。產生這兩個bug的根本原因就在於前面提到過的:

這裡需要特別註意的是其中粉色線的長度才是真實線的長度,當stroke-linecap的值為round或square時,視覺上會使線的頭尾加長。

0%時的問題

這個問題非常明顯,我進度明明是0,確有進度明顯不合理。產生這種問題的原因就是當strokeLinecap不為默認值butt時,首尾會超出來一截。

解決問題的方式也非常簡單,那就是當strokeLinecap不為默認值butt 且 進度為0時讓strokeLinecap屬性等於默認值。

let cap = (strokeLinecap !== 'butt' && percent !== 0) ? strokeLinecap : 'butt';

// 在進度圓上將strokeLinecap屬性傳入的值替換為cap
//  <circle strokeLinecap={cap} ></circle>

96%時的問題

與上面0%問題類似,由於默認多出來的一截會導致進度還沒有到100%,隻有96%時就已經連到瞭一起,給人以100%的效果。

之所以會產生這種問題,是因為0%時明明沒有進度卻占用瞭一部分的進度值。這就導致原本100%進度對應100%周長變為瞭100%進度對應100%周長減去strokeLinecap額外帶來的周長。

根據觀察strokeLinecap額外帶來的周長約等於線的寬度strokeWidth,所以解決方案如下。

let strokeDasharray = "";
if(strokeLinecap === 'butt') {
    // 當前是默認狀態,采用全周長計算
    strokeDasharray=`${Math.floor(percent / 100 * perimeter)} ${perimeter}`;
} else {
    // 需要剪掉strokeLinecap多出來的那部分
    strokeDasharray=`${Math.floor(percent / 100 * (perimeter - strokeWidth))} ${perimeter}`;
}

進度條loading應用場景

隻要帶有進度的地方都可以使用它,比如常見的下載進度、xxx完成進度、加載頁面進度。也可以動態的加載從0到某個進度,常用於金融中自己的持倉占比等。

到此這篇關於利用svg實現帶加載進度的loading的文章就介紹到這瞭,更多相關svg帶加載進度loading內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: