快速构建进度环

Avatar of Jeremias Menichelli
Jeremias Menichelli

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 价值 200 美元的免费积分!

在一些特别重的网站上,用户需要暂时看到一个视觉提示,以表明资源和资产仍在加载,然后才能看到一个完整的网站。 有不同的方法来解决这种 UX 问题,从转轮到 骨架屏幕

如果我们使用一个开箱即用的解决方案,它可以为我们提供当前的进度,就像 Jam3 的 preloader 包 所做的那样,构建一个加载指示器就会变得更容易。

为此,我们将创建一个环/圆圈,对它进行样式设置,根据进度进行动画,然后将其包装在一个组件中以供开发使用。

步骤 1:让我们创建一个 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> 标签,在那里我们使用 r 属性声明环的半径,使用 cxcy 声明其在 SVG viewBox 中的中心位置,以及圆圈描边的宽度。

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

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

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

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

要完成环,我们需要将 fill 设置为 transparent,并为圆圈选择一个 stroke 颜色。

查看 CodePen 上 Jeremias Menichelli (@jeremenichelli) 的 SVG 环

步骤 2:添加描边

下一步是为我们环的外线长度添加动画,以模拟视觉进度。

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

stroke-dasharray

此属性类似于 border-style: dashed,但它允许您定义虚线的宽度和它们之间的间隙。

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

使用这些值,我们的环将有 10px 的虚线,它们之间相隔 20px。

查看 CodePen 上 Jeremias Menichelli (@jeremenichelli) 的 虚线 SVG 环

stroke-dashoffset

第二个属性允许您沿着 SVG 元素的路径移动此虚线-间隙序列的起点。

现在,想象一下,如果我们将圆圈的周长传递给 stroke-dasharray 的两个值。 我们的形状将有一个长虚线占据整个长度,以及一个相同长度的间隙,这个间隙将不可见。

这最初不会造成任何变化,但如果我们也为 stroke-dashoffset 设置相同长度,那么长虚线将完全移动并显示出间隙。

减小 stroke-dasharray 将开始显示我们的形状。

几年前,Jake Archibald 在 这篇文章 中解释了这种技术,这篇文章中还有一个实时示例,可以帮助您更好地理解它。 您应该去阅读他的教程。

周长

我们现在需要的是这个长度,它可以通过半径和这个简单的三角函数公式计算得出。

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.style.strokeDasharray = `${circumference} ${circumference}`;
circle.style.strokeDashoffset = circumference;

步骤 3:进度到偏移量

有了这个小技巧,我们知道,将周长值分配给 stroke-dashoffset 将反映 0 进度状态,而 0 值将表示进度已完成。

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

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

通过对属性进行过渡,我们将获得动画效果

.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%,
}

将所有这些内容放在一起将得到类似这样的东西。

查看 CodePen 上 Jeremias Menichelli (@jeremenichelli) 的 vegymB

在本例中添加了一个数字输入,以帮助您测试动画。

为了使其能够轻松地与您的应用程序结合使用,最好将解决方案封装在一个组件中。

作为 Web 组件

现在我们已经有了逻辑、样式和 HTML,我们可以轻松地将其移植到任何技术或框架。

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

class ProgressRing extends HTMLElement {...}

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

这是自定义元素的标准声明,它扩展了本机 HTMLElement 类,该类可以通过属性进行配置。

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

在元素的构造函数中,我们将创建一个影子根,以封装样式及其模板。

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 定义,当(在本例中)progress 被修改时,该 getter 将触发 attributeChangeCallback

查看 CodePen 上 Jeremias Menichelli (@jeremenichelli) 的 ProgressRing Web 组件

此 CodePen 在撰写本文时只在 Chrome 中有效。 添加了一个间隔以模拟进度变化。

作为 Vue 组件

Web 组件很棒。 也就是说,一些可用的库和框架,比如 Vue.js,可以完成很多繁重的工作。

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

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

编写单个文件组件也是可能的,而且可能更简洁,但我们采用工厂语法来匹配最终的代码演示。

我们将属性定义为 props,并将计算定义为 data。

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>
`

当我们在应用程序中更新元素的 progress prop 时,Vue 会负责计算更改并更新元素样式。

查看 CodePen 上 Jeremias Menichelli (@jeremenichelli) 的 Vue ProgressRing 组件

注意:添加了一个间隔以模拟进度变化。 我们在下一个示例中也会这样做。

作为 React 组件

与 Vue.js 类似,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 prop 来计算 `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` prop 的改变会触发一个新的渲染周期,重新计算 `strokeDashoffset` 变量。

查看 CodePen 上 Jeremias Menichelli 的 React ProgressRing 组件 (@jeremenichelli)。

总结

此解决方案的实现方法基于 SVG 形状和样式、CSS 过渡以及少量 JavaScript 代码来计算特殊属性以模拟绘制圆周。

一旦我们分离出这部分代码,就可以将其移植到任何现代库或框架中,并将其包含在我们的应用程序中。在这篇文章中,我们探讨了 Web Components、Vue 和 React。

进一步阅读