【css】如何实现环形进度条
最近团队的童鞋接到了一个有关环形进度条的需求,想要还原一个native的沿环轨迹渐变进度条的效果,看到这个效果的时候,笔者陷入了沉思。。
环形进度条的效果,最先想到的就是使用CSS利用两个半圆的hack来模拟实现的:
<div class='circle-container'> <div class="circle-item"> <div class="circle-left-wrap"> <div class="left"></div> </div> <div class="circle-right-wrap"> <div class="right" style="transform: rotate(70deg)"></div> </div> <div class='mask'></div> </div> </div> <div class='circle-container'> <div class="circle-item"> <div class="circle-left-wrap"> <div class="left" style="transform: rotate(70deg)"></div> </div> <div class="circle-right-wrap"> <div class="right" style="transform: rotate(180deg)"></div> </div> <div class='mask'></div> </div> </div> <style> .circle-container{ position: relative; float: left; width: 120px; height: 120px; } .circle-item{ position: absolute; left: 10px; top: 10px; width: 100px; height: 100px; border-radius: 50%; background-color: #59d; } .circle-left-wrap, .circle-right-wrap{ position: absolute; left: 0; top: 0; width: 50px; height: 100px; overflow: hidden; } .circle-right-wrap{ left: 50px; } .left, .right { position: absolute; top: 0; left: 0; width: 100px; height: 100px; border-radius: 50%; background: #ddd; } .left{ clip: rect(0, 50px, auto, 0); } .right{ clip: rect(0, auto, auto, 50px); left: -50px; } .mask{ position: absolute; top: 5px; left: 5px; width: 90px; height: 90px; border-radius: 50%; background-color: #fff; } </style>
代码如上所示,实现起来并不复杂,主要是利用遮挡关系和clip属性,具体效果如下:
其中值得一提的是,虽然这种实现很巧妙,但是:
1、使用上两个半圆各控制一般的进度,需要js中间做一次转换,操作起来并不算太方便;
2、实现上由于本身的限制,虽然可以实现带渐变的进度条(只需要简单修改最外层容器背景色,效果见下图),但是由于本身遮挡关系的实现机制,并不能实现“纯透明”的无进度部分;
3、而且css的实现上似乎并不支持沿环形的渐变;
.circle-item{ position: absolute; left: 10px; top: 10px; width: 100px; height: 100px; border-radius: 50%; background: linear-gradient(to top left, #f63, yellow); }
基于以上原因,笔者尝试了一些其他的实现方式,先进入笔者视野的就是svg,因为工作中其实用得并不算多,也正常趁此次机会实践一下:
<style> svg{ -webkit-mask-image: linear-gradient(to top left, rgba(0,0,0,0), rgba(0,0,0,1)); } svg:last-of-type{ position: absolute; top: 0; left: 0; -webkit-mask-image: linear-gradient(to top left, rgba(0,0,0,1), rgba(0,0,0,0)); } </style> <div class='container'> <i class="circle"></i> <svg width="440" height="440" viewbox="0 0 440 440"> <circle cx="220" cy="220" r="195" stroke-width="50" stroke="yellow" fill="none" transform="matrix(0,-1,1,0,0,440)" stroke-dasharray="700 1069"></circle> </svg> <svg width="440" height="440" viewbox="0 0 440 440"> <circle cx="220" cy="220" r="195" stroke-width="50" stroke="#f63" fill="none" transform="matrix(0,-1,1,0,0,440)" stroke-dasharray="700 1069"></circle> </svg> </div>
由于没有采用之前的“遮挡hack”的方式实现,所以实现能完全没有css实现的不能实现部分透明的缺陷,而且进度条的控制可以使用stroke-array很方便的控制。看上去好像是最好的实现呢?确实每种方案都有优劣,svg也不是全能:
1、由于svg渐变仍然是作用于标签主体的,似乎并不能作用于stroke(目前笔者并没有查到相关的资料,如果错误,烦请留言),那么环的渐变要如何实现呢?笔者使用了mask-image,然后通过叠加两个同样进度的circle标签,改变它们alpha通道上的渐变防线,实现的和之前CSS一样的渐变效果;
2、svg本身实践上也有一点小问题,当然这是其本身机制所致,并不算是实现的缺陷:
1)svg 的witdh和height与viewbox第三、第四个参数存在着对应关系(这就和我们之前提到的缩放的关系是一样的,请特别注意);
2)circle的cx、cy实际上是计算边框的,也即是如果圆范围为440*440,那么中点就在(220,220);
3)circle的半径r在计算时指的是从圆心到边框中心的距离,以上述代码为例,边框为50的circle,它的半径应该是220 - 50 / 2 = 195;
4)由于circle的起点并不是通常的图形的最上部而是最右侧(也就是3点钟方向),所以还需要做一点的transform;
当然,撇开一些svg的使用特性,svg的这套方案还是蛮不错的,不过由于mask-image和svg本身的兼容性上有一些问题,使用的时候还是需要谨慎对待。
既然说到了svg,那就不得不提提canvas的实现了,笔者虽然自己也简单了实现了一下,不过看到codepen上面一个很酷炫的实例。。就不放上自己的献丑了:
当然这个例子并不是一个环形进度条的例子(从原名就能看出来XD),还有很多多余的粒子效果,不过从实现上来讲,canvas的实现确实非常酷炫。
看似问题好像都要解决了,但是我们似乎忘了什么?最开始的需求是要实现沿圆环的旋转方向渐变。。但是以上的两种方式都是纯线性渐变和圆环本身是没关系的。。有没有办法实现呢?答案当然是有的,先撇开canvas可以自己订制画笔画出的颜色不谈,回到渐变上,我们之前一直使用线性渐变,但是css中还有另一种渐变——锥形渐变,但是它目前还在草案阶段,实现的厂商也并不多(笔者仅用canary的实验属性中见过),那么就不能实现了么?答案当然也是否定的,想想之前的clip,再想想渐变,是不是有什么神奇的事情就要发生呢!?
是不是觉得很神奇,让我们来看看具体如何实现的:
<style> .wheel, .colors, .color { content: ""; position: absolute; border-radius: 50%; width: 9.5em; height: 9.5em; } .wheel { display: block; z-index: 1; box-shadow: inset 0 16px 32px 14px rgba(0, 0, 0, 0.7); overflow: hidden; } .colors { list-style: none; position: relative; -webkit-filter: blur(10px); transform: rotate(170deg) scaleX(-1); } .color { clip: rect(0px, 9.5em, 9.5em, 4.75em); } .color:after { content: ""; position: absolute; border-radius: 50%; left: calc(50% - 4.75em); top: calc(50% - 4.75em); width: 9.5em; height: 9.5em; clip: rect(0px, 4.75em, 9.5em, 0px); } .color:nth-child(1):after { background-color: #9ED110; z-index: 12; transform: rotate(30deg); } .color:nth-child(2):after { background-color: #50B517; z-index: 11; transform: rotate(60deg); } .color:nth-child(3):after { background-color: #179067; z-index: 10; transform: rotate(90deg); } .color:nth-child(4):after { background-color: #476EAF; z-index: 9; transform: rotate(120deg); } .color:nth-child(5):after { background-color: #9f49ac; z-index: 8; transform: rotate(150deg); } .color:nth-child(6):after { background-color: #CC42A2; z-index: 7; transform: rotate(180deg); } .color:nth-child(7):after { background-color: #FF3BA7; z-index: 6; transform: rotate(180deg); } .color:nth-child(8):after { background-color: #FF5800; z-index: 5; transform: rotate(210deg); } .color:nth-child(9):after { background-color: #FF8100; z-index: 4; transform: rotate(240deg); } .color:nth-child(10):after { background-color: #FEAC00; z-index: 3; transform: rotate(270deg); } .color:nth-child(11):after { background-color: #FFCC00; z-index: 2; transform: rotate(300deg); } .color:nth-child(12):after { background-color: #EDE604; z-index: 1; transform: rotate(330deg); } .color:nth-child(n+7) { transform: rotate(180deg); } .left-wrapper,.right-wrapper{ position: absolute; left: 0; top: 0; width: 50%; height: 100%; overflow: hidden; } .left-circle,.right-circle{ position: absolute; top: 0; left: 0; width: 9.5em; height: 100%; border-radius: 50%; background: #ddd; } .left-circle{ clip: rect(0, 4.75em, auto, 0); } .right-circle{ clip: rect(0, auto, auto, 4.75em); left: -4.75em; } .right-wrapper{ left: 50%; } .wheel-mask{ position: absolute; top: 5%; left: 5%; width: 90%; height: 90%; border-radius: 50%; background-color: #fff; } </style> <div class="wheel"> <ul class="colors"> <li class="color"></li> <li class="color"></li> <li class="color"></li> <li class="color"></li> <li class="color"></li> <li class="color"></li> <li class="color"></li> <li class="color"></li> <li class="color"></li> <li class="color"></li> <li class="color"></li> <li class="color"></li> </ul> <div class='left-wrapper'> <div class="left-circle" style="transform:rotate(70deg)"></div> </div> <div class='right-wrapper'> <div class="right-circle" style="transform:rotate(180deg)"></div> </div> <div class="wheel-mask"></div> </div>
其实进度条的实现和之前并没有什么差别,最大的就是在于背景的看着像锥形渐变的一块区域的实现,这其实是csstrick上的一个有关实现锥形渐变的小技巧,原理是:
1、利用clip迭代出各种纯色的三角形区域;
2、利用filter将纯色区域模糊化,达到类似渐变的效果;
其实最开始就是上图的效果,然后添加一个filter:blur(10px),模糊边界,看起来就变成了这样:
剩下的事情也就水到渠成了。
到最后,我们来简单总结一下用到的技术:
首先是CSS的相关遮挡原理,当然这个原理离不开一个。。一个已经在标准里被废弃的属性——clip,满屏的部分支持令人觉得倍感尴尬,不过我们用到的clip的功能主要也就是裁剪,所以在短时间内并不会有太大的影响。
那么简单的过一下api:
clip:rect(top, right, bottom, left);
如左图所示,虚线的矩形便是实际裁剪的矩形的区域,右图则是锥形色块的实现原理:首先通过父容器将显示区域裁成只有右边一半,再在子元素里用clip把左边的元素也裁掉,在没有旋转的情况下,就变成了什么都看不到,然后通过rotate和li标签间的遮挡关系来最终实现锥形的色块。
最后的最后还有个小插曲,以上的代码如果在.wheel中简单的改动一下:
.wheel { display: block; z-index: 1; box-shadow: inset 0 16px 32px 14px rgba(0, 0, 0, 0.7); margin: 250px 0 0 250px; overflow: hidden; }
其实只是添加了一段margin,但是在某些android手机(实验发现有问题的手机是s6 edge,系统版本6.0)上却呈现了奇怪的效果(表现为模糊的色块不能完全显示或者直接不能显示)。笔者开始觉得是合成层的问题,强制开启了硬件加速之后确实好了,但是后来发现,去掉那一行margin也能恢复正常,但是加上就会导致整个着色区域在一个以屏幕左上角为顶点有一定宽高的矩形范围内,如图:
多番查找之后仍没有找到解决方案,(也许css filter在堪忧的性能确实使用得比较少也是原因之一吧XD),暂时就先留在本文中,后续有了解决方案再更新吧。
2018.01.03补充:
最近看到了一个纯CSS实现的一个饼图,虽然不是不是完全的环形的,但是感觉也挺有意思的,研究下来发现了一个一直不太关注的知识点:
border-radius
相信很多同学对这个属性并不陌生,期初笔者也是这么想的,直到遇到了要用一个矩形的容器画一个半圆的时候:
脑中如果浮现的code是border-radius: 0 50% 50% 0的同学,你得到的将是下面
再让我们来看看border-radius的语法是怎样的:
border-radius: <length-percentage>{1,4} [ / <length-percentage>{1,4} ]?
where <length-percentage> = <length> | <percentage>
可以看到,其实border-radius的完整参数应该是有两组的,而我们平常只写了一组,前面那一组代表的是水平半径,后面则代表垂直半径,当后面的参数省略的时候,将使用前面一组的copy作为缺省值,可能大家心里都不太清楚何谓水平和垂直半径,其实这个border-radius的原理是画一个半径为参数值的“椭圆”,当我们作用在正方形的容器上时,垂直和水平半径是一样的,所以通常就忽略了,而当容器为一个矩形的时候,我们这需要考虑其每个顶点的椭圆的半径了,比如如下的三个例子:
左起第一个图:正方形容器宽高均为80px,当设置border-radius:50%时,等价于
border-radius: 40px 40px 40px 40px/40px 40px 40px 40px;
相当于画了4个长轴长、短轴长也就是水平半径和垂直半径相同的四个“椭圆”;
第二个图:在一个高80px宽40px的容器中,当设置border-radius: 50%时,等价于
border-radius: 20px 20px 20px 20px/40px 40px 40px 40px;
效果就相当于画了4个长轴长(垂直半径)为40px,短轴长(水平半径)为20px的椭圆;
第三个图:在一个高80px宽40px的容器中,当设置border-radius: 0 100% 100% 0/0 50% 50% 0时,等价于(注:此处的图片有误,实际上应该用下面的属性画出来的是只有右边的半圆)
border-radius: 0 40px 40px 0/0 40px 40px 0;
效果就相当于画了2个水平垂直半径相同的圆,如虚线的示意,于是就得到了我们想要的用矩形画一个半圆的效果。
其中有关相对单位的转换笔者就不再赘述了,最后再放上那个饼图的实际效果。