快速构建一个圆形的进度条

特别声明:此篇文章内容来源于@JEREMIAS MENICHELLI的《Building a Progress Ring, Quickly》一文。

在一些特别生的网站上,用户需要一个可视化的是示,以表明网站资源仍然在加载。从Spinner到Skeleton屏幕有不同的方法来解决这类的用户体验效果。

如果我们使用的是开箱即用的解决方案,它为我们提供了当前的进度,比如Jam3所提供的预加载程序包,那么构建一个加载指示器就变得更容易了。

为此,我们将制作一个带有动画的环形(圆形)的进度条,然后将其包装成一个组件,再提供给用户使用。

使用SVG制作一个环形

通过使用HTML和CSS来绘制一个环形(圆圈)有很多种方法,在这个教程中我选择了SVG,因为它可以通过属性配置和样式,可以适配所有屏幕。

<svg class="progress-ring" height="120" width="120">
    <circle class="progress-ring__circle" stroke-width="1" fill="transparent" r="58" cx="60" cy="60" />
</svg>

<svg>元素中,我们放了一个<circle>元素,制作了一个以cxcy为中心位置(圆心),r为半径的圆环,并且通过stroke-width设置了圆环的边框的粗细。

你可能已经注意到了,圆环的半径是58而不是60,这看起来是正确的。我们需要减去描边的宽度,否则圆环将会溢出SVG的容器。

radius = (width / 2) - (strokeWidth * 2)

这意味着,如果我们把描边增加到4,那么半径应该是52

52 = (120 / 2) - (4 * 2)

所以它看起来像一个圆环,我们需要将它的fill设置为透明,并为圆环选择一个描边的颜色(stroke):

<svg class="progress-ring" width="120" height="120">
    <circle class="progress-ring__circle" stroke="white" stroke-width="4" fill="transparent" r="52" cx="60" cy="60"/>
</svg>

添加描边

下一步是让我们的环的边框的动来来模拟视觉上的进度条效果。

我们将使用两个你可能没有听说过的CSS属性,因为它们是SVG元素专有的属性stroke-dasharraystroke-dashoffset

stroke-dasharray

这个属性就像border-style: dashed,但它可以让你定义破折号的宽度和它们之间的距离。

.progress-ring__circle {
    stroke-dasharray: 10 20;
}

stroke-dashoffset

这个属性允许你沿着SVG元素的路径移动这个dash-gap序列的起始点。

现在,想象一下,如果我们把圆的周长传递给stroke-dasharray。我们的形状将会有一个长划占据整个长度和一个不可见的相同长度的空隙。

这将不会导致最初的改变,如果我们也给stroke-dashoffset设置相同的长度,那么长破折号将会移动所有的方式并显示出差距。

减少的stroke-dasharray会开始显示我们的形状。

几年前,@Jake Archibaled在这篇文章中解释了这个技巧,它也有一个生动的例子,可以帮助你更好的理解它,你应该花点时间好好阅读这篇文章。

周长

我们现在需要的是可以用半径和这个简单的三角公式计算的需要的长度。

circumference = radius * 2 * PI

因为我们知道圆环的半径是52

326.7256 ~= 52 * 2 * PI

我们也可以通过JavaScript得到这个值:

const circle = document.querySelector('.progress-ring__circle');
const radius = circle.r.baseVal.value;
const circumference = radius * 2 * Math.PI;

这样我们就可以为我们的circle元素添加样式:

circle.style.strokeDasharray = `${circumference} ${circumference}`;
circle.style.strokeDashoffset = circumference;

进度条设置

通过这个小技巧,我们知道将周长值分配给stroke-dashoffset,表示进度条零进展状态,而0值表示进度(进程)条已完成。

因此,随着进度的增长,我们需要减少这样的偏移量:

function setProgress(percent) {
    const offset = circumference - percent / 100 * circumference;
    circle.style.strokeDashoffset = offset;
}

添加transition属性,这样看上去有动画的感觉:

.progress-ring__circle {
    transition: stroke-dashoffset 0.35s;
}

关于stroke-dashoffset一个特别之处是:它的起点是垂直居中,水平方向为右。为了得到想要的效果,有必要对圆形进行负值的旋转。

.progress-ring__circle {
    transition: stroke-dashoffset 0.35s;
    transform: rotate(-90deg);
    transform-origin: 50% 50%,
}

最终的效果如下:

改变输入框的数字,可以帮助你更好的测试动画效果。

为了便于应用程序内部进行耦合,最好将解决方案封装成一个组件。

Web组件

对于这样的一个环形进度条,我们已经有了逻辑、样式和对应的HTML结构,我们可以轻松地将它移植到任何技术或框架中。

首先,让我们使用Web组件。

class ProgressRing extends HTMLElement {...}

window.customElements.define('progress-ring', ProgressRing);

这是一个自定义元素的标准声明,扩展了原生的HTML元素类,它可以通过属性来配置。

<progress-ring stroke="4" radius="60" progress="0"></progress-ring>

在元素的构造函数中,我们将创建一个shadow root来封装样式及其模板。

constructor() {
    super();

    // get config from attributes
    const stroke = this.getAttribute('stroke');
    const radius = this.getAttribute('radius');
    const normalizedRadius = radius - stroke * 2;
    this._circumference = normalizedRadius * 2 * Math.PI;

    // create shadow dom root
    this._root = this.attachShadow({mode: 'open'});
    this._root.innerHTML = `
        <svg
        height="${radius * 2}"
        width="${radius * 2}"
        >
            <circle
                stroke="white"
                stroke-dasharray="${this._circumference} ${this._circumference}"
                style="stroke-dashoffset:${this._circumference}"
                stroke-width="${stroke}"
                fill="transparent"
                r="${normalizedRadius}"
                cx="${radius}"
                cy="${radius}"
            />
        </svg>

        <style>
            circle {
                transition: stroke-dashoffset 0.35s;
                transform: rotate(-90deg);
                transform-origin: 50% 50%;
            }
        </style>
    `;
}

你可能已经注意到,我们并没有把值硬编码到SVG中,而是从传递给元素的属性中获取这些值。

此外,我们还计算了环的周长,并在开始之前设置了stroke-dasharraystroke-dashoffset

下一步是观察progress属性并修改圆形的样式。

setProgress(percent) {
    const offset = this._circumference - (percent / 100 * this._circumference);
    const circle = this._root.querySelector('circle');
    circle.style.strokeDashoffset = offset; 
}

static get observedAttributes() {
    return [ 'progress' ];
}

attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'progress') {
        this.setProgress(newValue);
    }
}

setProgress是一个类方法,当progress属性更改时将会调用这个方法。

observedAttributes是由一个静态getter定义的,它将触发attributeChangeCallback,在这种情况之下,progress将被修改。

这个示例在写这篇文章时在Chrome上,并且添加了一个interval来模拟progress更改。

Vue组件

Web组件是很强大。但是用一些可用的库和框架,比如Vue,能让我们变得更简单很多。

首先,我们需要定义视图(View)组件。

const ProgressRing = Vue.component('progress-ring', {});

编写单个文件组件也是可以的,而且还更简洁,但我们接下来的示例彩用了工厂语法来写。

我们将属性定义为作为数据的props和计算。

const ProgressRing = Vue.component('progress-ring', {
    props: {
        radius: Number,
        progress: Number,
        stroke: Number
    },
    data() {
        const normalizedRadius = this.radius - this.stroke * 2;
        const circumference = normalizedRadius * 2 * Math.PI;

        return {
            normalizedRadius,
            circumference
        };
    }
});

由于计算属性在Vue中支持开箱即用,我们可以使用它来计算stroke-dashoffset的值。

computed: {
    strokeDashoffset() {
        return this._circumference - percent / 100 * this._circumference;
    }
}

接下来,我们将SVG作为模板添加。请注意,这里的简单部分是Vue为我们提供了绑定,将JavaScript表达式引入到属性和样式中。

template: `
    <svg
        :height="radius * 2"
        :width="radius * 2"
    >
        <circle
        stroke="white"
        fill="transparent"
        :stroke-dasharray="circumference + ' ' + circumference"
        :style="{ strokeDashoffset }"
        :stroke-width="stroke"
        :r="normalizedRadius"
        :cx="radius"
        :cy="radius"
        />
    </svg>
`

当我们更新app中元素的progress时,Vue将会负责计算并更新元素样式。

注意,添加一个间隔来模拟progress更改。我们在下一个示例中也会这样做。

React组件

类似于Vue的方式,React可以帮助我们处理所有的配置和计算值,这要感谢props和JSX语法。

首先,我们得到一些从props传递下来的数据。

class ProgressRing extends React.Component {
    constructor(props) {
        super(props);

        const { radius, stroke } = this.props;

        this.normalizedRadius = radius - stroke * 2;
        this.circumference = this.normalizedRadius * 2 * Math.PI;
    }
}

我们的模板是组件的render函数的返回值,在这里我们使用progress来计算stroke-dashoffset的值。

render() {
    const { radius, stroke, progress } = this.props;
    const strokeDashoffset = this.circumference - progress / 100 * this.circumference;

    return (
        <svg
        height={radius * 2}
        width={radius * 2}
        >
            <circle
                stroke="white"
                fill="transparent"
                strokeWidth={ stroke }
                strokeDasharray={ this.circumference + ' ' + this.circumference }
                style={ { strokeDashoffset } }
                stroke-width={ stroke }
                r={ this.normalizedRadius }
                cx={ radius }
                cy={ radius }
                />
        </svg>
    );
}

progress的更改将触发新的圆形周长渲染,会重新计算strokeDashoffset变量。

总结

这个解决方案的方法是基于SVG的形状和样式,CSS的transition和少量的JavaScript计算特殊属性来模拟绘制的图形的周长。

一旦我们将这一小部分分开,我们就可以将它移植到任何现代的库或框架中,并将其包含在我们的应用中,在本文中我们探讨了Web组件,Vue和React组件。

大漠

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

如需转载,烦请注明出处:https://www.w3cplus.com/svg/building-progress-ring-quickly.html

返回顶部