制作Loading组件

发布于 大漠

最近项目中使用到一个Loading效果,其实是一个很简单的效果,主要是因为这个Loading出现在不同的场景之中,而且大小也不一致。对于这样的效果,往往都会想通过组件的方式来处理,其出发点就是更易维护,易扩展。当然,这对于前端的同学而言并没有什么复杂性,也没有多少技术含量。不过我还是希望把这个过程记录下来。

咱们先来看一个截图:

从上图可以看出来,其效果是一样的,不同之处是使用场景不同,大小不同而以。那么接下来,就来聊聊这样的一个效果怎么通过不同的方式来完成。

实现原理

实现上面的这样的一个效果,我们需要有一点点数学相关的知识,这样更有易于后续效果制作。比如说,示例中有14个圆点,那么这14个圆点具有自己相关的特性参数:

  • 圆点的半径,比如说30px
  • 圆点的颜色,比如说#f36
  • 圆点的位置,按一定的比例分布在一个容器上,并且围成一个圆形

比如图所示:

注意,上较绘制的不是很标准,只是为了阐述问题。这里将会使用到一些数学公式,因为我们需要知道每个圆点的圆心的位置。

继续简化一下,就如下图这样:

这里会运用到一些角度和弧度相关的知识,其实这部分知识点,在学习Canvas的时候有所涉猎。在CSS中,咱们做旋转一般使用的是deg(角度)为单位,但在JavaScript绘制圆或圆弧却常用弧度rad为单位。

一个完整的圆的弧度是,所以2π rad = 360°1 π rad = 180°1°=π/180 rad1 rad = 180°/π(约57.29577951°)。以度数表示的角度,把数字乘以π/180便转换成弧度;以弧度表示的角度,乘以180/π便转换成度数。

rad = (π / 180) * deg

同样的:

deg = (rad * 180) / π

使用JavaScript来实现角度和弧度之间的换算。一个π大约是3.141592653589793,在JavaScript中对应的是Math.PI。那么角度和弧度之间的换算:

rad = (Math.PI * deg) / 180

同样的:

deg = (rad * 180) / Math.PI

下图展示了常见的角度和弧度之间的换算:

接下来回到我们的示例中来,示例有14个圆点,那么其每个圆点对应的位置可以通过下面的公式计算出来。首先计算出每个点对应的rad值。

rad = Math.PI * deg / 180

根据上面的公式,我们需要知道deg。众所周知,一个圆是360deg,我们在这个圆上平均布了14个点,那么每个圆对应的deg值是

deg = 360 / 14 * i

其中i是一个从0 ~ 13的索引值。套到对应的公式中:

rad = Math.PI * 360 / 14 * i / 180

在JavaScript中,使用一个for循环,可以打印出其值:

for (let i = 0, len = 14; i < len; i++) {
    let rad = Math.PI * 360 / 14 * i / 180
    console.log(`第${i+1}个圆点对应的rad值:${rad}`)
}

根据上面的计算得到每个圆点对应的rad值,接下来就需要利用三角函数相关的知识,来计算每个圆点圆心的(x,y)值。

换成JavaScript中的数学公式:

dotX = Math.cos(rad) * r
dotY = Math.sin(rad) * r

假设外圆的容器半径r = 100。继续将上面的值放到for循环中:

for (let i = 0, len = 14; i < len; i++) {
    let rad = Math.PI * 360 / 14 * i / 180
    console.log(`第${i+1}个圆点对应的rad值:${rad}`)
    let dotX = Math.cos(rad) * 100
    let dotY = Math.sin(rad) * 100
    console.log(`第${i+1}个圆点对应坐标值:(${dotX},${dotY})`)        
}

最后通过CSS的transformtranslate()可以将14个圆点围成一个圆圈。此时,如果你的效果中不是14个圆点,而是五个圆点的时候,你只需要修改相关的参数即可。

圆点排列,通过上面的公式计算,已经搞定。现在是需要一个动画效果,让你的圆点变得更为有趣。也就是说,只有配上了动效,才是我们最终需要的Loadingu效果。接下来,来看一下,圆点的动效。

比如,我们有一个简单的动效,这个动效是通过keyframes来完成的。具体效果可以根据自己的需要来做,这里只是一个简单的效果:

@keyframes ball-spin {
    0%,
    100% {
        opacity: 1;
        transform: scale(1);
    }
    20% {
        opacity: 1;
    }
    80% {
        opacity: 0;
        transform: scale(0);
    }
}

不用我解释,大家一看就明白。假设我们的动画的持续时间是1s。效果如下:

如果你要的是这样的一个效果,那到这里,理论上是完成了。可我们要的效果是这样的:

要实现这样的一个效果,我们需要对每个圆点的animation-delay做一些处理。前面提到过,整个动画的animation-duration1s,那么每个圆点的animation-delay依此延迟1/14 s。如此一来,咱们可以计算出时间:

-1 * (1 + (i + 1) * 1 / 14)

注意:此处的-1控制的是方向,让你能感觉到loading效果是顺时针还是逆时针转。系数为-1表时逆时针,就是上图看到的效果。

整个运用到的原理就如上所述。接下来分别看看几种不同方式的实现。

纯CSS实现方式

Loading效果实现方式有很多种,比如@css_live整理这些Loading动效的Demo,就涵盖了多种实现方式,有纯CSS的、有Canvas的,也有SVG的。而且在CodePen上也有很多Loading的Demo这里的Demo演示

这里我们先用纯CSS的方式来完成。我们需要一个完成动效的HTML结构:

<div class="loading">
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
    <div><span></span></div>
</div>

div.loading下的div + span对应的个数就是你所需要的圆点数。这里使用了14个。为什么要使用div+span样的嵌套标签,简单的解释一下。因为圆点有两个效果:

  • 位置排列,使用的是transfrom中的translate
  • 动效使用的是transform中的scale()

如果我们只用一个div,那么圆点的排列和动效中同时使用到的transform较为难控制。所以为了让实现方式更简单,牺牲了结构,嵌套了一个span标签。也就是排列用在div上,动效用在span上。

众所周知,纯CSS是不具备动态计算的能力,如果我们要实现上面的效果,就要人肉的去计算。不过值得庆幸的是,有很多优秀的CSS处理器,比如说Sass、LESS之类的都具备动态计算,也具备上面所说的数学函数和for循环的特性。这样一来,事情就变得简单多了。

详细代码就不全贴了,整几个关键的代码片段。首先根Demo效果所需要的参数,声明几个变量:

$loadingSize: 200px;    // Loading容器的大小
$dotRadius: 24px;       // 圆点半径  
$dotNums: 14;           // 圆点个数(需要和div.loading中子元素div个数对应起来)
$dotColor: #f36;        // 圆点颜色

另外for循环以及圆点位置相关的代码如下:

.loading {
    width: $loadingSize;
    height: $loadingSize;
    color: $dotColor;
    transform-origin: center;

    div {
        color: $dotColor;
        width: $dotRadius;
        height: $dotRadius;
        margin-top: $dotRadius / 2;
        margin-left: $dotRadius / 2;
        
        span {
            width: $dotRadius;
            height: $dotRadius;
            animation: ball-spin 1s infinite ease-in-out;  
            
            background-color: currentColor;
            border: 0 solid currentColor;
        }
     
        @for $i from 1 through 14 {
            &:nth-child(#{$i}) {
                transform: translate(cos(($i - 1) * 360deg / $dotNums) * $loadingSize / 2, sin(($i - 1) * 360deg / $dotNums) * $loadingSize / 2);
                
                & > span {
                    animation-delay: -(1 + $i * 1 / $dotNums) * 1s
                }
            }
        }
    }
}

对应一些三角函数的代码这里就不列出来了。如果你感兴趣的话可以使用sass-math。你也可以将示例中相应的函数像SassMagic一样放到一个Sass的仓库中,方便之后使用。

最终的效果如下:

虽然效果出来了,但其可扩展性相对而言较为麻烦一点,比如说我要一个小一点的,个数少一点的,颜色不一样的。那么需要重新修改前面声明的变量,然后再编译出相应的代码,修改对应的HTML结构。感兴趣的同学可以试试。

CSS自定义属性

CSS自定义属性已经是很成熟的CSS属性了。在小站上也有多篇文章介绍了有关于CSS自定义属性。为什么会想到CSS自定义属性呢?因为能使用CSS处理器变量的,就可以使用CSS自定义属性。但在这里有一个蛋疼的地方,就是CSS自定义属性调用通过var()函数来完成,然后计算要依赖于calc()函数。那么要把其结合Sass这样的处理器来做,就基本上是无解(至少我现在没找到可解的方案)。但CSS自定义属性有一个特性setProperty(),可以使用它来完成CSS自定义属性的动态变化。

因此,使用CSS自定义属性方案来实现Loading的动画效果的时候,我借用了原生JavaScript的手段来完成圆点的排列和动画的计算。为了能更好的展示不同的Loading效果,接下来的Demo提供了一些可配置参数。这些可配置参数,让你来动态修改CSS自定义属性的值。先上效果吧:

你可以像下面这样,修改Demo右侧的一些参数,实现不一样的Loading效果,如下图所示:

为了节省篇幅,也将只贴一些关键的代码:

:root {
    --loadingRadius: 168px;
    --dotRadius: 24px;
    --dotColor: #d8d8d8;
}

.loading{
    width: var(--loadingRadius);
    height: var(--loadingRadius);
    color: var(--dotColor);

    transform-origin: center;
    
    div {
        width: var(--dotRadius);
        height: var(--dotRadius);
        color: var(--dotColor);
        margin-top: calc((var(--dotRadius) / 2) * -1);
        margin-left: calc((var(--dotRadius) / 2) * -1);
    }

    span {
        animation: ball-spin 1s infinite ease-in-out;  
        
        width: var(--dotRadius);
        height: var(--dotRadius);
    }
}

对应的JavaScript代码如下:

const style = document.documentElement.style;
var rangs = {
    dotNums: document.getElementById('dotNums'),
    loadingRadiusVal: document.getElementById('loadingRadius'),
    dotRadiusVal: document.getElementById('dotRadius'),
    dotColorVal: document.getElementById('dotColor')
}

// 创建修改CSS自定义属性的函数
function valueChange(id, value) {
    style.setProperty('--' + id, value);
}

// 动态创建div.loading下的子元素div+span
function insertHtml() {
    let loadingWrap = document.getElementById('loading');
    var dots = rangs.dotNums.value;
    
    for(let i = 0; i < dots; i++) {
        let divEle = document.createElement('div');
        let spanEle = document.createElement('span');
        divEle.appendChild(spanEle);
        loadingWrap.appendChild(divEle)
    }  
}

// 右侧参数面板的控制
rangs.loadingRadiusVal.addEventListener('input', function(e){
    valueChange(e.currentTarget.id, e.currentTarget.value + 'px');
})
rangs.dotRadiusVal.addEventListener('input', function(e){
    valueChange(e.currentTarget.id, e.currentTarget.value + 'px');
})
rangs.dotColorVal.addEventListener('input', function(e){
    valueChange(e.currentTarget.id, e.currentTarget.value);
})

function transformDot() {
    let dotNums = document.querySelectorAll('.loading > div');
    let loadingRadiusVal = rangs.loadingRadiusVal.value;
    let dotRadiusVal = rangs.dotRadiusVal.value;
    let dotColorVal = rangs.dotColorVal.value;
    
    // 计算圆点的位置和动效
    for (let i = 0, len = dotNums.length; i < len; i++) {
        let rad = 2 * Math.PI / dotNums.length  * i;
        let dotX =  Math.cos(rad) * loadingRadiusVal / 2;
        let dotY =  Math.sin(rad) * loadingRadiusVal / 2;
        dotNums[i].style.transform = `translate(${dotX}px,${dotY}px)`;
        dotNums[i].firstElementChild.style.animationDelay = -1 * (1 + (i + 1) * 1 / dotNums.length) + 's';
    }
}

insertHtml();
transformDot();

// 修改参数后修改动效
rangs.dotNums.addEventListener('input', function(e){
    let loadingWrap = document.getElementById('loading');
    loadingWrap.innerHTML = '';
    insertHtml();
    transformDot();
})

对应的HTML模板,这里就不展示了。

其实我还在想,这个效果,我们应该也可以使用CSS Houdini来完成。如果你对CSS Houdini有一定的了解,不仿尝试写写。如果你对CSS Houdini一点都不了解,建议点击这里先了解一下,我想你会从此喜欢上她的。

Vue写Loading组件

除了上面的方法之外,为了更好的使用,还可以将其写成一个组件。比如一个Vue组件,当然喜欢使用其他JavaScript框架的同学也可以使用别的,比如React。这里用的是Vue。

首先创建一个Vue组件,你可以使用单的一个文件,也可以使用组件模板。因为Demo是在Codepen上展示的,我就使用了一个组件模板:

<template id="loading">
    <div class="loading">
        <div v-for="(dotNum, index) in dotNums" :key="index" :style="dotTransform(index, dotNums)">
        <span :style="dotAimation(index, dotNums)"></span>
        </div>
    </div>
</template>

Vue.component('loading',{
    template: '#loading',
    props: {
        loadingRadiusVal: {
            type: Number,
            required: true,
            default: 168
        },
        dotRadiusVal: {
            type: Number,
            required: true,
            default: 24
        },
        dotColorVal: {
            type: String,
            required: true,
            default: '#d8d8d8'
        },
        dotNums: {
            type: Number,
            required: true,
            default: 10
        }
    },
    methods: {
        dotTransform: function(index, dotNums) {
            let rad = 2 * Math.PI / dotNums  * index;
            let dotX =  Math.cos(rad) * this.loadingRadiusVal / 2;
            let dotY =  Math.sin(rad) * this.loadingRadiusVal / 2;
            return {
                transform: `translate(${dotX}px,${dotY}px)`
            };
        },
        dotAimation: function(index, dotNums) {
            let delayTime = `${-1 * (1 + (index + 1) * 1 / dotNums) }s`
            return {
                animationDelay: delayTime
            }
        }
    }  
})

有了这个组件的时候,咱们可以这样调用:

<loading :dot-color-val="dotColor" :dot-nums="dotNums" :loading-radius-val="loadingRadius" :dot-radius-val="dotRadius" :style="changeStyle"></loading>

let app = new Vue({
    el: '#app',
    data () {
        return {
        loadingRadius: 168,
        dotRadius: 20,
        dotColor: '#ff3366',
        dotNums: 12
        }
    },
    computed: {
        changeStyle: function() {
            let rootEle = document.documentElement;
            
            rootEle.style.setProperty('--loadingRadius', `${this.loadingRadius}px`)
            rootEle.style.setProperty('--dotRadius', `${this.dotRadius}px`)
            rootEle.style.setProperty('--dotColor', this.dotColor)
        }
    }
})

和前面的示例一样,也提供了一个参数控制面板,不过在Vue中参数控制面板控制CSS自定义属性就容易的多了,采用v-model的双向绑定即可。最终效果如下:

因为是Vue的初学者,如果写得不好,还请各咱大神多多指正。如果你也是Vue的爱好者或初学者,可以和我一起来学习Vue相关的知识。当然,Vue写的各式各样的Loading组件非常的多,比如这里就收集了很多

总结

这篇文章主要介绍了如何使用不同的方式来实现一个Loading的动效。说实在的,前端就是这样,实现一个效果有很多种方式方法。不同层次的同学可以使用不同的方式。就好比这篇文章中介绍的。不懂JavaScript的可以使用纯CSS(最好你对CSS处理器有所了解),懂JavaScript的话,你可以使用JS,配合一些优秀的CSS特性。当然,为了更易于维护和扩展,也可以借助Vue这样的框架,将整个效果封装成一个组件。

那么整个效果的实现方式不同,但其原理是一样的。对于初学者而言,最关键的是要掌握原理。只有掌握了原理部分,你才能根据自己的环境选择实现方案。

如果上面有不对之处,或者你有更好的方案,欢迎在下面的评论中与我一起共享。如果这篇文章对你有所帮助,可以赏杯咖啡,鼓励我继续创作。(^_^)!!!

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/animation/loading-animation-component.htmlNike Hypervenom Phantom