跳到主要內容
黯羽輕揚每天積累一點點

圓環進度條

免費2016-12-10#CSS#Solution#渐变边框#CSS进度条#CSS圆形进度条#CSS圆环进度条#css circular progress bar#css gradient border

怎樣實現一個漸變色的圓環進度條

一.場景

需要實現一個漸變色的圓環進度條,進度條長這樣子:

[caption id="attachment_1238" align="alignnone" width="200"]circle circle[/caption]

0 進度時顯示一個灰色的槽,載入過程中彩色條順時針走完一圈

二.經典解法「兩扇門」

圓環進度條有一種經典解法,原理很有意思,HTML 結構如下:

<div class="circle progress-basic">
    <div class="left">
        <div class="circle left-circle"></div>
    </div>
    <div class="right">
        <div class=" circle right-circle"></div>
    </div>
</div>

先不考慮圓,想像一個雙開門,progress-basic 是門框,left 是左半扇門,right 是右半扇門,如下:

.progress-basic {
    position: relative;
}
.left, .right {
    position: absolute;
    width: 100px; height: 200px;
    top: 0;
    overflow: hidden;
}
.left {
    left: 0;
}
.right {
    right: 0;
}

然後考慮如何實現 0-180deg 的效果:進度條需要從 0 點位置一點點出現,填滿右邊半圓環,180deg 的時候左邊半圓環灰槽,右邊半圓環進度條

想像正方形紙上左邊畫了一個半圓環,用左手遮住左邊,然後順時針旋轉紙,半圓環就會從左手指尖一點點出現,轉夠 180 度時,正好左邊是空白,右邊是半圓環。此時不能再轉了,會露餡(半圓環進度條剛好走完,再轉就斷了)

180-360deg 的方法與之類似,先把半圓環進度條藏好,再一點一點轉出來。完整 CSS 如下:

.circle {
    width: 200px; height: 200px;
    box-sizing: border-box;
    border-radius: 50%;
    border: 10px solid silver;
}

.progress-basic {
    position: relative;
}
.left, .right {
    position: absolute;
    width: 100px; height: 200px;
    /*塞进父级border*/
    top: -10px;
    overflow: hidden;
}
.left {
    /*塞进父级border*/
    left: -10px;
}
.right {
    /*塞进父级border*/
    right: -10px;
}
.left-circle, .right-circle {
    border-color: #077df8;
}
.left-circle {
    margin-right: -100px;
    border-left-color: transparent;
    border-top-color: transparent;
    transform: rotateZ(-45deg);
}
.right-circle {
    margin-left: -100px;
    border-right-color: transparent;
    border-bottom-color: transparent;
    transform: rotateZ(-45deg);
}

門框自帶灰色槽,兩扇門 overflow: hidden,左半扇門藏好右半圓環進度條,右半扇門藏好左半圓環進度條。先轉右邊的紙,紙上左邊的半圓環一點點出現,到 180 度後去轉左邊的紙,紙上右邊的半圓環一點點出現,360 度時剛好拼接出完整圓環。如下:

var $ = document.querySelector.bind(document);

void function() {
    var $rightCircle = $('.right-circle');
    var $leftCircle = $('.left-circle');
    var prog = 0;
    var deg = 0;
    var initialDeg = -45;
    var halfDone;
    var timer = setInterval(function() {
        prog += 0.01;
        deg = 360 * prog;
        if (deg < 180) {
            $rightCircle.style.transform = 'rotateZ(' + (initialDeg + deg) + 'deg)';
        }
        else if (deg < 360) {
            if (!halfDone) {
                console.log('half done');
                $rightCircle.style.transform = 'rotateZ(' + (initialDeg + 180) + 'deg)';
                halfDone = true;
            }
            $leftCircle.style.transform = 'rotateZ(' + (initialDeg + deg - 180) + 'deg)';
        }
        else {
            console.log('done');
            $leftCircle.style.transform = 'rotateZ(' + (initialDeg + 180) + 'deg)';
            clearInterval(timer);
        }
    }, 100);
}();

很巧妙的方法,不知道有沒有學名,暫且稱之為「兩扇門」

P.S. 透過 clip 也能實現,原理一樣

三.嘗試經典解法

「兩扇門」原理好像通用,照搬到漸變進度條上,那麼需要把進度條圖片水平翻轉,再分為左右兩半,如圖:

[caption id="attachment_1239" align="alignnone" width="404"]circle-left-right circle-left-right[/caption]

具體如下:

<div class="circle circle-image progress-basic">
    <div class="left">
        <div class="circle left-circle"></div>
    </div>
    <div class="right">
        <div class=" circle right-circle"></div>
    </div>
</div>

/*用渐变图片左右拼*/
.circle-image .left-circle {
    margin-right: -100px;
    border: none;
    background: url(circle-right.png) no-repeat 100px 0;
    -webkit-background-size: 100px 200px;
    background-size: 100px 200px;
    transform: rotateZ(0deg);
}
.circle-image .right-circle {
    margin-left: -100px;
    border: none;
    background: url(circle-left.png) no-repeat 0 0;
    -webkit-background-size: 100px 200px;
    background-size: 100px 200px;
    transform: rotateZ(0deg);
}

存在一個致命問題:180 度時候另一半接不上,純色場景下左右兩半圓環顏色一樣,看不到 180-360deg 時的差異,不存在接不上的問題,但漸變圓環很明顯能看到 180 度後左邊圓環是整個轉著出現的,而不是一點點出現。嘗試失敗,考慮其他方式

四.白塊遮罩

另一種不同的思路是把進度條圓環先鋪在下面,左右都用白塊遮住,然後順時針旋轉右邊白塊,底下的右半圓環一點點出現,180 度換左邊白塊轉動,底下的左半圓環一點點出現

這樣就不存在左右接不上的問題,因為圓環本來就是完整的。解決了條的問題,還要考慮槽,可以把槽畫在左右白塊上,旋轉白塊槽跟著轉,進度條一點點出現,完美。具體如下:

<div class="circle circle-block progress-basic">
    <div class="left">
        <div class="block left-block">
            <div class="circle"></div>
        </div>
    </div>
    <div class="right">
        <div class="block right-block">
            <div class="circle"></div>
        </div>
    </div>
    <div class="circleBar"></div>
</div>

/*用白块遮住*/
.circle-block .block {
    width: 100%; height: 100%;
    background: #fff;
}
.circle-block .right-block {
    -webkit-transform-origin: 0 50%;
    /*盖住露出的边缘*/
    border-right: 1px solid #fff;
}
.right-block .circle {
    margin-left: -100px;
    border-left-color: transparent;
    border-top-color: transparent;
    transform: rotateZ(-45deg);
}
.circle-block .left-block {
    -webkit-transform-origin: 100% 50%;
    /*盖住露出的边缘*/
    border-left: 1px solid #fff;
    margin-left: -1px;
}
.left-block .circle {
    border-right-color: transparent;
    border-bottom-color: transparent;
    transform: rotateZ(-45deg);
}
.circle-block .circleBar {
    width: 200px; height: 200px;
    margin-top: -10px; margin-left: -10px;
    background: url(circle.png) no-repeat 0 0;
    -webkit-background-size: 200px 200px;
    background-size: 200px 200px;
}

在高解析度設備上,白塊遮得不夠完美,因此需要:

/*盖住露出的边缘*/
border-right: 1px solid #fff;

效果還不錯,但存在很明顯的限制:只適用於純色背景,因為白塊的背景色要和頁面背景色一致,否則就露餡了

五.canvas 描邊

條件允許的話,可以用 canvas 畫一個,所有問題迎刃而解:

<canvas class="canvas">不支持canvas</canvas>

void function() {
    var options = {
        size: 200,
        lineWidth: 10,
        rotate: 0
    };

    var canvas = $('.canvas');
    var ctx = canvas.getContext('2d');
    canvas.width = canvas.height = options.size;

    // 重置原点到中心点
    ctx.translate(options.size / 2, options.size / 2);
    // 旋转-90度,让x轴与-y方向重合
    ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI);

    var radius = (options.size - options.lineWidth) / 2;

    var drawCircle = function(color, lineWidth, percent) {
            percent = Math.min(Math.max(0, percent || 1), 1);
            ctx.beginPath();
            ctx.arc(0, 0, radius, 0, Math.PI * 2 * percent, false);
            ctx.strokeStyle = color;
            // butt, round or square
            // 线条两端的形状
            // butt 默认,两端不加装饰,截断感
            // round 给线条两端包上半圆
            // square 给线条两端包上半方,看起来和butt一样,但比butt长一个lineWidth
            ctx.lineCap = 'butt';
            ctx.lineWidth = lineWidth;
            ctx.stroke();
    };

    // 灰槽
    drawCircle('#efefef', options.lineWidth, 100 / 100);

    var prog = 0;
    // 色条渐变
    var gradient = ctx.createLinearGradient(0, 100, 0, -100);
    gradient.addColorStop(0, '#ff0000');
    gradient.addColorStop(0.15, '#ff00ff');
    gradient.addColorStop(0.33, '#0000ff');
    gradient.addColorStop(0.49, '#00ffff');
    gradient.addColorStop(0.67, '#00ff00');
    gradient.addColorStop(0.84, '#ffff00');
    gradient.addColorStop(1, '#ff0000');
    // '#077df8'
    var timer = setInterval(function() {
        prog += 1;
        // 色条
        drawCircle(gradient, options.lineWidth, prog / 100);
        if (prog >= 360) {
            clearInterval(timer);
        }
    }, 100);
}();

注意漸變的定義:

var gradient = ctx.createLinearGradient(0, 100, 0, -100);

這樣定義的其實是從右向左的線性漸變,因為座標系經過了兩次變換:

// 重置原点到中心点
// 原点平移到(100, 100)
ctx.translate(options.size / 2, options.size / 2);
// 旋转-90度,让x轴与-y方向重合
// x轴y轴转置
ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI);

很容易讓進度條變成圓頭的:

ctx.lineCap = 'round';
// 线条两端的形状
// butt 默认,两端不加装饰,截断感
// round 给线条两端包上半圆
// square 给线条两端包上半方,看起来和butt一样,但比butt长一个lineWidth

但 canvas 描邊在高解析度設備上存在鋸齒的問題,需要一點小技巧:

var width = canvas.width, height=canvas.height;
if (window.devicePixelRatio) {
    canvas.style.width = width + "px";
    canvas.style.height = height + "px";
    canvas.height = height * window.devicePixelRatio;
    canvas.width = width * window.devicePixelRatio;
    ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}

幾倍屏就用幾倍圖,這樣可以解決鋸齒問題

六.svg 描邊動畫

同樣,如果條件允許,還可以用 svg 描邊動畫來實現,如下:

<div class="progress-svg">
    <!-- 条 -->
    <svg viewBox="-10 -10 220 220">
    <g fill="none" stroke-width="10" transform="translate(100,100)">
    <path d="M 0,-100 A 100,100 0 0,1 86.6,-50" stroke="url(#cl1)"/>
    <path d="M 86.6,-50 A 100,100 0 0,1 86.6,50" stroke="url(#cl2)"/>
    <path d="M 86.6,50 A 100,100 0 0,1 0,100" stroke="url(#cl3)"/>
    <path d="M 0,100 A 100,100 0 0,1 -86.6,50" stroke="url(#cl4)"/>
    <path d="M -86.6,50 A 100,100 0 0,1 -86.6,-50" stroke="url(#cl5)"/>
    <path d="M -86.6,-50 A 100,100 0 0,1 0,-100" stroke="url(#cl6)"/>
    </g>
    </svg>
    <!-- 槽 -->
    <svg viewBox="-10 -10 220 220">
    <path d="M200,100 C200,44.771525 155.228475,0 100,0 C44.771525,0 0,44.771525 0,100 C0,155.228475 44.771525,200 100,200 C155.228475,200 200,155.228475 200,100 Z" stroke-dashoffset="629"></path>
    </svg>
</div>
<!--  Defining Angle Gradient Colors  -->
<svg width="0" height="0">
<defs>
<linearGradient id="cl1" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="1" y2="1">
    <stop stop-color="#618099"/>
    <stop offset="100%" stop-color="#8e6677"/>
</linearGradient>
<linearGradient id="cl2" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="0" y2="1">
    <stop stop-color="#8e6677"/>
    <stop offset="100%" stop-color="#9b5e67"/>
</linearGradient>
<linearGradient id="cl3" gradientUnits="objectBoundingBox" x1="1" y1="0" x2="0" y2="1">
    <stop stop-color="#9b5e67"/>
    <stop offset="100%" stop-color="#9c787a"/>
</linearGradient>
<linearGradient id="cl4" gradientUnits="objectBoundingBox" x1="1" y1="1" x2="0" y2="0">
    <stop stop-color="#9c787a"/>
    <stop offset="100%" stop-color="#817a94"/>
</linearGradient>
<linearGradient id="cl5" gradientUnits="objectBoundingBox" x1="0" y1="1" x2="0" y2="0">
    <stop stop-color="#817a94"/>
    <stop offset="100%" stop-color="#498a98"/>
</linearGradient>
<linearGradient id="cl6" gradientUnits="objectBoundingBox" x1="0" y1="1" x2="1" y2="0">
    <stop stop-color="#498a98"/>
    <stop offset="100%" stop-color="#618099"/>
</linearGradient>
</defs>
</svg>

對應的 CSS 如下:

.progress-svg {
  display: inline-block;
  position: relative;
  text-align: center;
}
.progress-svg svg {
  width: 200px; height: 200px;
}
.progress-svg svg:nth-child(2) {
  position: absolute;
  left: 0;
  top: 0;
  -webkit-transform: rotate(-90deg);
          transform: rotate(-90deg);
}
.progress-svg svg:nth-child(2) path {
  fill: none;
  stroke-width: 25;
  stroke-dasharray: 629;
  stroke: #fff;
  opacity: .9;
  -webkit-animation: load 10s;
          animation: load 10s;
}
@keyframes load {
  0% {
    stroke-dashoffset: 0;
  }
}

svg 描邊動畫與 path 的兩個東西有關:

  • stroke-dasharray:虛線每小段長度

  • stroke-dashoffset:虛線初始位置偏移量(向左偏移

向左偏移很關鍵,否則想不通為什麼要遞減到 0,而不是從 0 遞增:

A dashed stroke with a non-zero dash offset. The dashing pattern is 20,10 and the dash offset is 15. The red line shows the actual path that is stroked.

[caption id="attachment_1240" align="alignnone" width="307"]stroke-dashoffset stroke-dashoffset[/caption]

詳細見 SVG 規範

原理是讓 stroke-dasharray 長度大於 path,並讓 stroke-dashoffset 等於 stroke-dasharray,此時沒有描邊(向左偏移把虛線的第一段實線弄沒了),然後讓偏移量遞減到 0,視覺效果就是虛線的第一段實線從左邊一點點露出來,為 0 時恰好覆蓋 path

反過來想,如果讓 stroke-dashoffset 為 0,stroke-dasharray 從極小值遞增至等於 path,雖然虛線第一段確實是一點點變長的,但後續小段沒辦法隱藏掉,所以不能用來實現描邊動畫

但是,svg 漸變描邊存在拼接的問題,仔細看能發現圓環上有 4 處斷開,就是因為兩段漸變接不上,雖然定義的漸變色值沒有問題,理論上能完美接起來,但實際效果不理想

同樣用 CSS 做漸變邊框也存在左右拼接的問題,如下:

<div class="colorfulBorder"></div>

.colorfulBorder {
    width:100px; height:100px; -webkit-transform:rotate(90deg);
}
.colorfulBorder:before {
    content:"";
    display:block;
    width:100px; height:50px;
    margin-top:10px; padding:10px; padding-bottom:0; box-sizing:border-box;
    border-top-left-radius:50px;
    border-top-right-radius:50px;
    background:-webkit-gradient(
        linear,
        left top,
        right top,
        color-stop(0,#fff),
        color-stop(1,#fff)
    ),-webkit-gradient(
        linear,
        left top,
        right top,
        color-stop(0,#077df8),
        color-stop(1,#74baff)
    );
    background-clip:content-box,padding-box;
}
.colorfulBorder:after {
    content:"";
    display:block;
    width:100px; height:50px;
    padding:10px; padding-top:0;
    box-sizing:border-box;
    border-bottom-left-radius:50px;
    border-bottom-right-radius:50px;
    background:-webkit-gradient(
        linear,
        left top,
        right top,
        color-stop(0,#fff),
        color-stop(1,#fff)
    ),-webkit-gradient(
        linear,
        left top,
        right top,
        color-stop(0,#fff),
        color-stop(1,#74baff)
    );
    background-clip:content-box,padding-box;
}

這種瑕疵導致整個方案不可行

七.線上 Demo

Demo 位址: http://www.ayqy.net/temp/progress/circle.html

八.總結

對於純色圓環進度條,建議採用經典「兩扇門」解法,能夠適應非純色背景

如果是漸變圓環進度條,方案選擇如下:

  • 條件允許,優選 canvas,能夠適應非純色背景

  • 不行就用白塊遮罩,但只適用於純色背景

svg 描邊動畫可以實現自定義形狀進度條,比如一條彎彎的小河,但不適用於漸變描邊的情況(漸變相接處有縫隙)

CSS 透過多 background 實現漸變邊框不可行(因為漸變相接處有很明顯縫隙)

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論