一、背景和意义
loading图标是前端使用频率较高的图标之一,前端在向后端请求数据时,在数据返回之前经常要先展示一下loading图标。ElementUI中就有v-loading属性用于展示loading图标,对应的文档页为:https://element.eleme.cn/2.13/#/zh-CN/component/loading
,大致效果为:
文本大致介绍其loading图标是怎么实现的,掌握了loading图标的实现方法后,我们也可以不用引入ElementUI,自己就可以直接写一个loading图标。
二、抽取loading图标相关代码
使用chrome开发者工具可以查看到loading图标的HTML代码:
把相关的HTML代码抽取出来,得到内容如下:
html
<style>
.el-loading-mask {
position: absolute;
z-index: 2000;
background-color: hsla(0,0%,100%,.9);
margin: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
transition: opacity .3s;
}
.el-loading-spinner {
top: 50%;
margin-top: -21px;
width: 100%;
text-align: center;
position: absolute;
}
.el-loading-spinner .circular {
height: 42px;
width: 42px;
animation: loading-rotate 2s linear infinite;
}
.el-loading-spinner .path {
animation: loading-dash 1.5s ease-in-out infinite;
stroke-dasharray: 90,150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: #409eff;
stroke-linecap: round;
}
@keyframes loading-rotate {
to {
transform: rotate(1turn)
}
}
@keyframes loading-dash {
0% {
stroke-dasharray: 1,200;
stroke-dashoffset: 0
}
50% {
stroke-dasharray: 90,150;
stroke-dashoffset: -40px
}
to {
stroke-dasharray: 90,150;
stroke-dashoffset: -120px
}
}
</style>
<div class="el-loading-mask">
<div class="el-loading-spinner">
<svg viewBox="25 25 50 50" class="circular">
<circle cx="50" cy="50" r="20" fill="none" class="path"></circle>
</svg>
</div>
</div>
将这一段代码保存成html文件,然后用浏览器打开,到看到如下效果:
在上面的代码中,<div class="el-loading-mask">
是遮罩层,<div class="el-loading-mask">
的作用是使loading图标位于页面正中间。现在我们主要关注loading图标的实现,可以将那两个div去掉,代码简化为:
html
<style>
.circular {
height: 42px;
width: 42px;
animation: loading-rotate 2s linear infinite;
}
.path {
animation: loading-dash 1.5s ease-in-out infinite;
stroke-dasharray: 90,150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: #409eff;
stroke-linecap: round;
}
@keyframes loading-rotate {
to {
transform: rotate(1turn)
}
}
@keyframes loading-dash {
0% {
stroke-dasharray: 1,200;
stroke-dashoffset: 0
}
50% {
stroke-dasharray: 90,150;
stroke-dashoffset: -40px
}
to {
stroke-dasharray: 90,150;
stroke-dashoffset: -120px
}
}
</style>
<svg viewBox="25 25 50 50" class="circular">
<circle cx="50" cy="50" r="20" fill="none" class="path"></circle>
</svg>
用浏览器打开,得到效果如下:
三、loading图标相关代码解读
3.1 svg部分解读
首先,代码中大概看出loading图标是用svg画出来的,另外还有两个keyframes动画序列。为了层层拆解,先利用chrome开发工具,将两个animation取消勾选,即禁掉动画,看到一个静态的四分之三圆,如图所示:
在HTML代码中,圆对应的元素是<circle>
,即<circle cx="50" cy="50" r="20" fill="none" class="path"></circle>
,其中cx="50" cy="50"
表示圆心坐标,即圆心的位置距离画布左边缘和上边缘的距离都是50像素,r="20"
表示圆的半径是20像素。fill="none"
表示圆是空心的,无填充颜色。
不过看到这里可能会有一个疑问,如果圆心坐标是(50, 50),半径是20,那么圆到左边缘和上边缘的距离就是30,应该看起来大于圆的半径了,但从页面效果看不是这样。这其实是svg中的viewBox="25 25 50 50"
这一属性起的作用,这一属性表示将以(25, 25)为左上角、长和宽都是50的矩形区域的内容缩放到整个svg画布。作为对比,我们把viewBox属性去掉,看到效果为:
圆只剩下一下角了,这是因为目前画布大小为42px,圆的大部分都在画布之外了。将画布调大一点,比如将width和height都设置为100%,另外再加上如下一段代码以红色标出以(25, 25)为左上角、长和宽都是50的矩形区域:
<div style="position: absolute;top: 25;left: 25;width: 50;height: 50;border: 1px solid red;"></div>
得到的效果如下:
初看红色矩形区域和圆所在的区域好像也对不上,这其实是由于body中有一个8px的margin导致的:
给body加上一个position: relative
属性,最后看到红色矩形区域和圆所在的区域差不太多:
viewBox="25 25 50 50"
是将上图的红色矩形区域缩放到整个画布,如果svg元素的长和宽大于50则会放大,如果小于50则会缩小。这样通过设置svg的width和height,就能等比例放大和缩小loading图标。
3.2 stroke相关属性解读
circle元素设置了如下几个stroke开头的CSS属性:
其中stroke-width: 2;
和stroke: #409eff;
相对比较好理解,分别设置了圆圈的宽度和颜色。stroke-linecap: round;
用于设置描边,图标小的时候其实看不出描边的效果,如果将svg调大一点,可以看到描边和不描边的效果差别:
stroke-dasharray: 90,150;
表示画圆的边时,先画长度为90px的线,再留长度为150px的空白。由于该圆的半径是20px,那么一圈的长度大概是3.14 * 20 * 2 = 125.6
,90px就是将近3/4圆。那这90px是从哪个点开始往哪个方向开始画呢,为了方便查看,可以将stroke-dasharray
属性改成10 5 30 1000
,该值表示先画10px的线,再留5px的空白,再画30px的线,后面的留1000px的空白,将看到如下效果:
显然,画圆时是先从右边中间处开始,沿着顺时针方向画。
最后一个stroke属性是stroke-dashoffset: 0;
,该属性表示偏移量,目前是设置为0,相当于没有偏移。沿用stroke-dasharray: 10 5 30 1000;
这一设置,一个正的偏移相当于往画线的相反方向拉拽画出的线:
如果将stroke-dashoffset设置为5,则第一根10px的蓝线应该就只剩下的一半了,看到的效果如下:
如果将stroke-dashoffset设置为10,则第一根10px的蓝线就刚好没了:
负的偏移则相当于往画线的方向拉拽画出的线:
将stroke-dashoffset设置为-10的效果如下:
10px和30px的线都还在,只是往画线方向移动了。
3.3 动画部分解读
第一个动画loading-rotate相对比较简单:
css
.circular {
...
animation: loading-rotate 2s linear infinite;
}
...
@keyframes loading-rotate {
to {
transform: rotate(1turn)
}
}
transform: rotate(1turn)
表示旋转一周,前面的2s linear infinite;
表示每2秒旋转一周,重复无限次。如果只保留loading-rotate一个动画,另外一个动画先禁用,看到的效果为:
第二个动画loading-dash相对更复杂一些:
css
.path {
animation: loading-dash 1.5s ease-in-out infinite;
...
}
...
@keyframes loading-dash {
0% {
stroke-dasharray: 1,200;
stroke-dashoffset: 0
}
50% {
stroke-dasharray: 90,150;
stroke-dashoffset: -40px
}
to {
stroke-dasharray: 90,150;
stroke-dasharray: -120px
}
}
先看@keyframes
部分,动画都是在stroke-dasharray和stroke-dasharray的不同值之间过渡,上一小节已经介绍了这两个属性的作用,这里不再详细解释。初始状态stroke-dasharray: 1,200; stroke-dashoffset: 0
对应的是从一个非常小的点开始:
然后过渡到stroke-dasharray: 90,150; stroke-dashoffset: -40px
这个状态:
在这两个状态之间,蓝线会一直变长,在第二个状态的时候达到最长。接下来蓝线会逐渐变短,过渡到了第三个状态,再次接近于成为一个蓝点:
这三个状态连起来就是这样的效果:
animation中的ease-in-out表示过渡效果是先缓慢地开始,然后加速,然后缓慢地结束。如果像之前的loading-rotate一样用linear则是这样的效果:
稍微显得有些机械和呆板,如果loading-dash也是用linear过渡效果,两个动画结合在一起的效果是这样:
跟原作相比是稍微差了一点点:
四、代码层面上的优化
ElementUI的loading图标的svg代码为:
html
<svg viewBox="25 25 50 50" class="circular">
<circle cx="50" cy="50" r="20" fill="none" class="path"></circle>
</svg>
其实viewBox也可以简化为以(0, 0)作为起点:
html
<svg viewBox="0 0 50 50" class="circular">
<circle cx="25" cy="25" r="20" fill="none" class="path"></circle>
</svg>
另外loading-dash的代码为:
html
@keyframes loading-dash {
0% {
stroke-dasharray: 1,200;
stroke-dashoffset: 0
}
50% {
stroke-dasharray: 90,150;
stroke-dashoffset: -40px
}
to {
stroke-dasharray: 90,150;
stroke-dasharray: -120px
}
}
单位px也可以省略,变成这样:
html
@keyframes loading-dash {
0% {
stroke-dasharray: 1,200;
stroke-dashoffset: 0
}
50% {
stroke-dasharray: 90,150;
stroke-dashoffset: -40
}
to {
stroke-dasharray: 90,150;
stroke-dasharray: -120
}
}
最终运行效果不变。