Element 2 组件源码剖析之Progress 环形/仪表盘进度条(二)

2,170 阅读7分钟

0x00 简介

前文 Progress线性进度条主要介绍了线性进度条的实现,本文将就环形/仪表盘进度<svg>实现进行解读,耐心读完,相信会对您有所帮助。

更多组件分析详见 👉 📚 Element UI 源码剖析组件总览

本专栏的 gitbook 版本地址已经发布 📚《learning element-ui》 ,内容同步更新中!

0x01 容器/画布

类名el-progress-circlediv元素创建环形进度条画布宽度(基于属性width)。

<div class="el-progress">
  <div class="el-progress-bar" v-if="type === 'line'"> 
    // ...
  </div>
  <div class="el-progress-circle" :style="{height: width + 'px', width: width + 'px'}" v-else>
    <svg viewBox="0 0 100 100">
      <path
        class="el-progress-circle__track"
        :d="trackPath"
        stroke="#e5e9f2"
        :stroke-width="relativeStrokeWidth"
        fill="none"
        :style="trailPathStyle"></path>
      <path
        class="el-progress-circle__path"
        :d="trackPath"
        :stroke="stroke"
        fill="none"
        :stroke-linecap="strokeLinecap"
        :stroke-width="percentage ? relativeStrokeWidth : 0"
        :style="circlePathStyle"></path>
    </svg>
  </div>
  <div
    class="el-progress__text"
    v-if="showText && !textInside"
    :style="{fontSize: progressTextSize + 'px'}"
  >
    <template v-if="!status">{{content}}</template>
    <i v-else :class="iconClass"></i>
  </div>
</div>

环形/仪表盘进度条使用了<svg>元素,元素的viewBox 属性允许指定一个给定的一组图形伸展以适应特定的容器元素。viewBox 属性的值是一个包含 4 个参数的列表 min-xmin-ywidth 、 height, 以空格或者逗号分隔开, 在用户空间中指定一个矩形区域映射到给定的元素,不允许宽度和高度为负值,0 则禁用元素的呈现。 此属性绝非三言两语能说清楚,在此不过多详述,推荐阅读 理解SVG viewport,viewBox,preserveAspectRatio缩放

<svg>元素实现,使用了两个 path 元素创建背景环el-progress-circle__track和进度环el-progress-circle__path

svg中的元素在浏览器渲染时会遵守html中有固定的排列顺序。先出现的元素会先绘制层级较低,而后绘制的元素层级依次增高,如果元素间的位置有重叠,则会现后绘制的元素会遮盖住先出现的元素(层级高遮盖层级低元素)。 渲染后图形叠加就实现了进度环效果。

image.png

对比可知环形/仪表盘的背景环、以及进度环起始都不一样,组件使用了功能更加强大灵活 <path> 标签来实现,但理解起来有点难度。若只是环形进度条可以使用<circle>标签实现会更加简单。

image.png

0x02 背景环 Path

接下先介绍几个基础且重要的计算属性。

属性 relativeStrokeWidth是圆形轮廓的宽度,值类型为 percentage, 是进度条的宽度跟画布宽度的百分比。 strokeWidth 默认值 6 , width 默认值 12, 所以 relativeStrokeWidth 默认值为 4.8 。

relativeStrokeWidth() { 
  return (this.strokeWidth / this.width * 100).toFixed(1);
},

属性 radius 是绘制圆形的半径,值类型为 percentage

radius() {
  if (this.type === 'circle' || this.type === 'dashboard') {
    // 画布一半 减去 轮廓宽度的一半
    return parseInt(50 - parseFloat(this.relativeStrokeWidth) / 2, 10);
  } else {
    return 0;
  }
}, 

属性 radius 是圆形的周长。

perimeter() {
  return 2 * Math.PI * this.radius;
},

属性 rate 表示绘制圆形的弧长,类型 dashboard时只会绘制 3/4 周长长度的轮廓,也就是仪表盘进度条为什么有缺口。

rate() {
  return this.type === 'dashboard' ? 0.75 : 1;
},

背景环 <path> 元素中属性d创建一个圆形;属性stroke-width指定了圆形的轮廓的宽度,绑定计算属性relativeStrokeWidth ;属性stroke定义了给定图形元素的外轮廓的颜色#e5e9f2;属性 fill 用于填充轮廓内的形状的颜色,值为none无填充;计算属性 trailPathStyle 会生成 stroke-dasharraystroke-dashoffset等属性用于设置描边的显示和偏移错位。

<path
  class="el-progress-circle__track"
  :d="trackPath"
  stroke="#e5e9f2"
  :stroke-width="relativeStrokeWidth"
  fill="none"
  :style="trailPathStyle">
</path>

fill 属性填充效果如下:

image.png

d属性

属性d使用计算属性trackPath,返回创建圆环的一系列路径描述。

trackPath() {
  const radius = this.radius;
  const isDashboard = this.type === 'dashboard';
  return `
    M 50 50
    m 0 ${isDashboard ? '' : '-'}${radius}
    a ${radius} ${radius} 0 1 1 0 ${isDashboard ? '-' : ''}${radius * 2}
    a ${radius} ${radius} 0 1 1 0 ${isDashboard ? '' : '-'}${radius * 2}
    `;
}, 

不同类型对应的路径描述。

// circle
M 50 50
m 0 -47
a 47 47 0 1 1 0 94
a 47 47 0 1 1 0 -94

// dashbord
M 50 50
m 0 47
a 47 47 0 1 1 0 -94
a 47 47 0 1 1 0 94

路径描述中用到了MovetoArcto这两个指令。

指令是大小写敏感的;一个大写的命令指明它的参数是绝对位置,而小写的命令指明相对于当前位置的点。可以指定一个负数值作为命令的参数:负角度将是逆时针的,绝对 x 和 y 位置将视为负坐标。负相对 x 值将会往左移,而负相对 y 值将会向上移。

Moveto 命令用作开始一个路径。可以被想象成拎起绘图笔,落脚到另一处。在上一个点和这个指定点之间没有线段绘制。

  • M x,y 在这里 x 和 y 是绝对坐标,分别代表水平坐标和垂直坐标。
  • m dx,dy 在这里 dx 和 dy 是相对于当前点的距离,分别是向右和向下的距离。

Arcto 命令用作绘制椭圆弧(圆是特殊的椭圆)。 指令格式 A/a (rx ry x-axis-rotation large-arc-flag sweep-flag x y)

  • A (绝对)| a (相对)
  • rx ry 是椭圆的两个半轴的长度。
  • x-axis-rotation 是椭圆相对于坐标系的旋转角度,角度数而非弧度数。
  • large-arc-flag 是标记绘制大弧(1)还是小弧(0)部分。
  • sweep-flag 是标记向顺时针(1)还是逆时针(0)方向绘制。
  • x y 是圆弧终点的坐标。 关于 large-arc-flagsweep-flag参数对应的绘制情况。因为绘制的是半圆,参数large-arc-flag的绘制大小弧对组件没有影响。

image.png

circle 路径图解

circle类型的路径指令解读如下:

  1. M 50 50 绘制从点位 1️⃣ 也就是圆心开始,绝对坐标 (50,50)
  2. m 0 47 从点位 1️⃣ 沿 y 轴下移 47,也就是圆的半径长度,移动到点位 2️⃣。
  3. a 47 47 0 1 1 0 -94 从点位 2️⃣ 到点位 3️⃣绘制一个半圆弧,从点位 2️⃣ 沿 y 轴上移 94(圆直径长度)就到点位 3️⃣。
  4. a 47 47 0 1 1 0 94 从点位 3️⃣ 到点位 4️⃣(也就是点位 2️⃣)绘制一个半圆弧,从点位 3️⃣ 沿 y 轴下移 94 就到点位 4️⃣,形成一个闭合圆。

image.png

dashbord 路径图解

dashbord类型的路径指令解读如下:

  1. M 50 50 绘制从点位 1️⃣ 也就是圆心开始,绝对坐标 (50,50)
  2. m 0 -47 从点位 1️⃣ 沿 y 轴上移 47,也就是圆的半径长度,移动到点位 2️⃣。
  3. a 47 47 0 1 1 0 94 从点位 2️⃣ 到点位 3️⃣绘制一个半圆弧,从点位 2️⃣ 沿 y 轴下移 94(圆直径长度)就到点位 3️⃣。
  4. a 47 47 0 1 1 0 -94 从点位 3️⃣ 到点位 4️⃣(也就是点位 2️⃣)绘制一个半圆弧,从点位 3️⃣ 沿 y 轴上移 94 就到点位 4️⃣,形成一个闭合圆。

image.png

不同类型的图形绘制起点(点位2️⃣),决定了进度条进度起点。当然dashbord类型到此还没有结束,它的缺口处理还没有。

缺口实现

stroke-dasharraystroke-dashoffset等属性可以直接用作一个 CSS 样式表内部的属性。 计算属性 trailPathStyle 会生成一个样式对象,用于设置描边的显示和偏移错位。

trailPathStyle() {
  return {
    strokeDasharray: `${(this.perimeter * this.rate)}px, ${this.perimeter}px`,
    strokeDashoffset: this.strokeDashoffset
  };
}, 
// circle
// { 
//     stroke-dasharray: 295.31px, 295.31px;
//     stroke-dashoffset: -36.9137px;
// }

// dashbord
// { 
//     stroke-dasharray: 221.482px, 295.31px;
//     stroke-dashoffset: -36.9137px;
// }

stroke-dasharray第一个属性值表是轮廓显示长度,circleperimeter 圆的周长,dashbord0.75*perimeter

image.png

计算属性strokeDashoffset用于起点的偏移,正数为x值向左偏移,负数为x值向右偏移。

strokeDashoffset() {
  const offset = -1 * this.perimeter * (1 - this.rate) / 2;
  return `${offset}px`;
}, 

相当于顺时针旋转圆,旋转缺口弧度的一半,形成一个对称结构。 image.png

0x03 进度环 Path

相较于背景环,进度环代码多了 stroke-linecap属性。

<path
  class="el-progress-circle__path"
  :d="trackPath"
  :stroke="stroke"
  fill="none"
  :stroke-linecap="strokeLinecap"
  :stroke-width="percentage ? relativeStrokeWidth : 0"
  :style="circlePathStyle">
</path>

跟背景环一样路径创建,不同之处进度展示通过stroke-dasharray 第一个属性值进行控制,通过绘制圆弧长度*percentage来显示进度。同时加入了transition缓动效果,使视感更加真实。

circlePathStyle() {
  return {
    strokeDasharray: `${this.perimeter * this.rate * (this.percentage / 100) }px, ${this.perimeter}px`,
    strokeDashoffset: this.strokeDashoffset,
    transition: 'stroke-dasharray 0.6s ease 0s, stroke 0.6s ease'
  };
},

属性 stroke-linecap 进度条边缘的形状为方形;
image.png

进度环的颜色使用计算属性stroke,支持自定义颜色、状态颜色,默认进度条背景色为#20a0ff

stroke() {
  let ret;
  if (this.color) {
    ret = this.getCurrentColor(this.percentage);
  } else { 
    switch (this.status) {
      case 'success':
        ret = '#13ce66';
        break;
      case 'exception':
        ret = '#ff4949';
        break;
      case 'warning':
        ret = '#e6a23c';
        break;
      default:
        ret = '#20a0ff';
    }
  }
  return ret;
},

percentage 值为 0 时,stroke-width属性值为0,元素将不绘制轮廓。

:stroke-width="percentage ? relativeStrokeWidth : 0"

0x04 外显文本

由前文可知,环形的内容显示为外显内容元素el-progress__text,计算属性content。若设置了进度条状态status,则显示图标。计算属性 iconClass 返回不同状态 success/exception/warning的icon, 不同类型下success/exception 对应图标有所区别。

  <div
    class="el-progress__text"
    v-if="showText && !textInside"
    :style="{fontSize: progressTextSize + 'px'}"
  >
    <template v-if="!status">{{content}}</template>
    <i v-else :class="iconClass"></i>
  </div>

外显文本样式使用绝对定位,在画布水平垂直居中。

.el-progress--circle .el-progress__text,
.el-progress--dashboard .el-progress__text {
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  text-align: center;
  margin: 0;
  -webkit-transform: translate(0, -50%);
  transform: translate(0, -50%);
}
.el-progress--circle .el-progress__text i,
.el-progress--dashboard .el-progress__text i {
  vertical-align: middle;
  display: inline-block;
}

0x05 📚参考&&关联阅读

"SVG/Element/path",MDN
"SVG/Attribute",MDN

0x06 关注专栏

此文章已收录到专栏中 👇,可以直接关注。