乐趣区

关于前端:可线性渐变的环形进度条的实现探究

本文次要介绍基于 NutUI Vue3 的 circleProgress 组件的设计与实现原理,是一个圆环形的进度条组件,用来展现操作的以后进度,反对批改进度以及渐变色。环形进度条是很罕用的一个组件,特地是在治理后盾数据统计的页面上或是一些须要用户期待的工作。

实现成果

成果如下图

实现思路

首先咱们来理一下咱们的需要,咱们须要一个 圆形的能够扭转进度,有动画的,能够反对渐变色的进度条

目前是有三种常见的实现计划:

  • CSS 实现
  • SVG 实现
  • Canvas 实现

而 SVG 又分为两种,一种是间接用 circle 实现,另外一种是用 path 来画进去。

而后咱们来看下一些国内闻名的组件库实现形式:

Antd Design Tdesign varlet Element UI vant
SVG Circle Circle Circle Path Path

Antd Design 和 TDesign,varlet 采纳 svg 的 Circle 实现,然而小编集体感觉 Antd Design 的渐变色的开始和完结是有问题的,TDesign 和 varlet 临时不反对环形进度条线性突变
Element UI 和 Vant 采纳 svg 的 Path 实现,Vant 是反对线性突变的,而且不存在渐变色的开始和完结不对应问题,Element 临时不反对线性突变

目前支流组件库的环形进度条基本上都是用 svg 绘进去的,因为实现思路简略,应用 SVG 画两个圆 一个圆作为底色,另一个圆作为进度展现,应用时候的问题也很少。NutUI 也抉择应用 SVG 来实现进度条。

下边具体介绍一下两种实现形式

circle 实现

首先来说下 SVG 的 circle (Antd,Tdesign 的实现形式),下边也会介绍到Antd 的进度条线性突变问题

首先第一步 咱们先来画个最简略的圆

<svg height="100" width="100" x-mlns="http://www.w3.org/200/svg">
  <circle r="40" cx="50" cy="50" stroke="'red'" stroke-width="10" fill="none" />
</svg>

上边的属性我就不多介绍了 不理解的同学其实也能够读出来大略意思,r 是半径,cx,cy 为圆点地位,以及色彩和弧度的宽度。
这里可能有的同学要问了 这不是一个 circle 就能够实现嘛,可不要遗记了咱们须要的成果,

那咱们再来画一个不是 100% 进度的圆,如何画呢
这里就用到了 stroke-dasharray 属性,能够将图形的描边进行点状化,这里须要了解的是,点状化的点,其大小是能够设置的,并不真的就是那么一个·,能够变长或者变短。
所以如果 circle 的点的长度正好等于 circle边长,那么点看上去就是 circle 的边。
咱们计算下圆环的周长就能够了,参数也就是弧长,极大值,极大值就是周长,弧长就是进度值。

大家应该也发现问题了,这样的话在不是 100% 进度的时候,局部弧度是没有色彩的,所以咱们还须要一个底色。也就是另外一个circle

<svg>
  <circle
    r="40"
    cx="50"
    cy="50"
    stroke="#d9d9d9"
    stroke-width="10"
    fill="none"
  />
  />
  <circle
    r="40"
    cx="50"
    cy="50"
    stroke="red"
    stroke-width="10"
    stroke-dasharray="200,251"
    fill="none"
    stroke-linecap="round"
  />
</svg>

因为这里咱们须要它从右边的两头地位开始,所以还须要加上旋转。

至于接下来就很简略了 让它动起来,那么如何动起来呢 动静扭转 stroke-dasharray 的值就能够了,下方介绍 path 的时候会讲到如何扭转。来讲一下遇到的小坑,就是咱们在做渐变色的时候,会发现咱们的渐变色并不是从咱们的进度开始中央开始突变的。
这里咱们也能够看下 Antd 的环形进度条突变,我用一个红到黑来给大家看一下。(Tdesign这里小编在在线编辑器里尝试了一下,线性突变没有失效。)

这里小编集体感觉 Antd 的这个突变也是不对的(仅代表集体想法),当然大家有别的想法也能够提出来一起探讨。

其实是因为线性突变是从左往右的,并且上边为圆环加了旋转的起因,解决办法大家能够自行搜索引擎搜寻一下,这里不多做论述。

path 实现

下边次要介绍一下用 pathVant,Element 实现办法)实现吧 能够简略并完满的解决上边的渐变色对应不上(Element 临时不反对线性突变)的问题。

circle 实现思路是一样的,画两个圆,一个用来示意底色,一个用来示意弧度,次要是咱们如何来画一个圆呢

  1. 理解 viewBox 属性,在 SVG 标签中增加该属性,这个属性是用来设置画布的大小,然而大家留神,它是一个绝对大小,会依据咱们的父元素扭转而动静适配。比方咱们将其属性设置为 viewBox="0,0,100,100", 其实它是将咱们的整个画布的宽和高分为 100 份,其中 SVG 元素是在这个宰割当前的画布上摆放展现。

    咱们不须要再关注 SVG 的宽高,它当初曾经实现了自适应,会主动依据外层父元素的宽高进行
    适配, 咱们最外层给用户一个 Props 来设置环形进度条的大小。

    <div :style="{height: radius * 2 +'px', width: radius * 2 +'px'}">
        <svg viewBox="0 0 100 100"></svg>
     </div>
  2. 理解 path 的 d 属性 ,既然咱们要用path 来画圆,那咱们当然得相熟一下🐂🍺的 d 属性来,它能够画出各种各样的线来。d 属性用来定义门路数据,咱们首先来理解下咱们须要用到的参数:

      M = moveto(M X,Y):将画笔挪动到指定的坐标地位
    
      A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):椭圆弧

    请留神 ⚠️,这些参数是辨别大小的,当它为大写的命令时表明它的参数是相对地位,小写的命令表明它的擦含混绝对于以后地位的点。咱们也能够应用负数值来作为咱们命令的参数。负绝对 x 值将会往左移,而负绝对 y 值将会向上移。

    下边来详解下咱们的这两个参数
    moveto属性其实很好了解,画笔的指定地位,x 轴和y 轴,M(相对) m(绝对)

    elliptical Arc 椭圆弧的记录以下:指令:A (相对) a (绝对)

    椭圆弧的参数模式:(rx ry x-axis- rotation large-arc-flag sweep-flag x y)详解参数:rx ry 是椭圆的两个半轴的长度。x-axis-rotation是椭圆绝对于坐标系的旋转角度,角度数而非弧度数。large-arc-flag 是标记绘制大弧 (1) 仍是小弧 (0) 局部。sweep-flag 是标记向顺时针 (1) 仍是逆时针 (0) 方向绘制。x y 是圆弧起点的坐标。

    因为咱们这里须要将圆开始的绘制方向交给用户来管制,所以这里来承受一个 props 来管制绘制方向。根据上述形容咱们能够写出 pathd 属性,
    在相对地位 50,50 的地位(圆心),先从圆心上方地位 45 的地位绘图(45 即为半径)而后接下来是椭圆弧的参数,rxry,半径为 45,旋转角度为 0,绘制一个大弧,而后是绘制方向,顺时针还是逆时针旋转,最初是圆弧起点的坐标。能够得出下边的代码

    const path = computed(() => {
      const isWise = props.clockwise ? 1 : 0;
      return `M 50 50 m 0 -45 a 45 45 0 1 ${isWise} 0 90 a 45 45 0 1, ${isWise} 0 -90`;
    });

    这样的话就会绘制出一个圆来,其余用到的属性就和上边的 cirlce一样了

stroke:描边色彩

stroke-width: 描边宽度

fill:填充色彩

stroke-dasharray: 距离多少像素绘制一次

既然 path 的门路咱们曾经实现了,接下来就是失常的给这个圆环上色和加上动静进度变动了。

首先咱们来写底部的背景圆环,用户能够自定义去传背景圆环弧度的色值以及圆弧的宽度。

 <path class="nut-circleprogress-path" :style="pathStyle" :d="path" fill="none" :stroke-width="strokeWidth">/>

 const pathStyle = computed(() => {
      return {stroke: props.pathColor};
});

接下来是展现进度条的圆环,因为咱们这里还须要一个渐变色,所以还须要在 SVG 中退出一些代码
查阅 SVG 的文档咱们找到了一个叫做 <linearGradient> 的 SVG 元素,通过应用该元素咱们能够达成色彩突变的目标。

  1. 创立 linearGradient
    在创立这个元素之前,咱们须要晓得 <linearGradient> 标签必须嵌套在 <defs> 的外部。<defs>标签是 definitions 的 缩写,它可对诸如突变之类的非凡元素进行定义。而且咱们必须给突变内容指定一个 id 属性,否则文档内的其余元素就不能引用它。为了让突变能被重复使用,突变内容须要定义在 <defs> 标签外部,而不是定义在形态下面。
  2. 设定色彩突变方向
    当初 <linearGradient> 元素创立胜利了,上面咱们能够为其赋值属性以达到按需要批改突变色彩变向的需要。
    突变的方向能够通过两个点来管制,它们别离是属性 x1x2y1y2,这些属性定义了突变路线走向。渐变色默认是程度方向的,然而通过批改这些属性,就能够旋转该方向。
  3. 设定渐变色

    <linearGradient> 中实践上增加的色彩是无下限的,但若想有突变成果起码要增加两种色彩。因而须要在 <linearGradient> 中创立起码两个 <stop> 元素,以增加你须要的色彩属性。
    所以咱们这里采纳一个循环来解决多个色彩属性

    <stop>元素有三个属性:

    stop-color:想要设定的突变色彩

    offset:在你定义的方向向量上,定义该色彩的失效地位,应用百分比来设置具体的存在地位。

    stop-opacity:设定 stop-color 色彩的透明度(临时用不到)

    <defs>
     <linearGradient :id="refRandomId" x1="100%" y1="0%" x2="0%" y2="0%">
           <stop v-for="(item, index) in stopArray" :key="index" :offset="item.key" :stop-color="item.value"></stop>
     </linearGradient>
    </defs>
    const stopArray = computed(() => {if (!isObject(props.color)) {return;}
       let color = props.color;
       const colorArr = Object.keys(color).sort((a, b) => parseFloat(a) - parseFloat(b));
       let stopArr: object[] = [];
       colorArr.map((item, index) => {
         let obj = {
           key: '',
           value: ''
         };
         obj.key = item;
         obj.value = color[item];
         stopArr.push(obj);
       });
       return stopArr;
     });

    渐变色加完当前,咱们来解决一下进度条(如何让进度条绑定上这个渐变色),其实很简略,将圆环的 stroke 变成 <linearGradient> 的惟一 id 就能够啦;接着解决一下圆环的进度展现,和上边 circle 的解决形式统一,应用 stroke-dasharray 就能够啦。

 <path
    class="nut-circleprogress-hover"
    :style="hoverStyle"
    :d="path"
    fill="none"
    :stroke-linecap="strokeLinecap"
    :stroke-width="strokeWidth"
  ></path>

const hoverStyle = computed(() => {
    let perimeter = 283;
    let offset = (perimeter * Number(props.progress)) / 100;
    return {stroke: isObject(props.color) ? `url(#${refRandomId})` : props.color,
        strokeDasharray: `${offset}px ${perimeter}px`
    };
});

上方的 stroke-linecap 属性是一个示意属性,定义了在关上子门路被描边时要在其开端应用的形态,能够用在 style 中。
至于上方的 283 是如何来的,其实很简略 就是咱们的圆环周长,2πr=2 3.1415926 45。
其实到了这里,咱们失常 h5 环境下的环形进度条就功败垂成了。

Taro 下的 SVG

因为 NutUI 是能够配合 Taro 用来开发微信小程序的,所以这里的环形进度条在小程序环境下当然也要领有雷同的性能了。

因为咱们一般 h5 环境下应用的 SVG 实现的,所以想一套代码实用,后果发现在小程序环境中暂不反对应用 SVG。

这是小程序官网文档中答复的,所以我这里采纳了将其作为背景图来展现。
咱们通过一些转换 SVG 为 base64 的网站发现,<其实是 %3C>%3E#替换成 %23 就能够啦,因为咱们里边还存在一些变量,所以我这边将他们拆离开了,分成几个变量来写。

<div :style="style"></div>

    const style = computed(() => {let { strokeWidth} = props;

      let stopArr: Array<object> = stop();
      let stopDom: string[] = [];
      if (stopArr) {stopArr.map((item: Item) => {
          let obj = '';
          obj = `%3Cstop offset='${item.key}' stop-color='${transColor(item.value)}'/%3E`;
          stopDom.push(obj);
        });
      }
      let perimeter = 283;
      let progress = +currentRate.value;
      let offset = (perimeter * Number(format(parseFloat(progress.toFixed(1))))) / 100;
      const isWise = props.clockwise ? 1 : 0;
      const color = isObject(props.color) ? `url(%23${refRandomId})` : transColor(props.color);
      let d = `M 50 50 m 0 -45 a 45 45 0 1 ${isWise} 0 90 a 45 45 0 1, ${isWise} 0 -90`;
      const pa = `%3Cdefs%3E%3ClinearGradient id='${refRandomId}' x1='100%25' y1='0%25' x2='0%25' y2='0%25'%3E${stopDom}%3C/linearGradient%3E%3C/defs%3E`;
      const path = `%3Cpath d='${d}' stroke-width='${strokeWidth}' stroke='${transColor(props.pathColor)}'fill='none'/%3E`;
      const path1 = `%3Cpath d='${d}' stroke-width='${strokeWidth}' stroke-dasharray='${offset},${perimeter}' stroke-linecap='round' stroke='${color}' fill='none'/%3E`;

      return {background: `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100'xmlns='http://www.w3.org/2000/svg'%3E${pa}${path}${path1}%3C/svg%3E")`,
        width: '100%',
        height: '100%'
      };
    });

你认为这就功败垂成了吗 不不不 你会发现你在动静扭转环形进度条的进度时,没有动画,显得僵硬刻板

所以咱们这里给它去减少一个动画成果,咱们这里去用 setTimeout 代替一下requestAnimationFrame(不理解的同学能够理解下这个属性,很好用的!), 因为小程序环境下不反对。

const requestAnimationFrame = function (callback: Function) {var currTime = new Date().getTime();
      var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
      lastTime = currTime + timeToCall;
      var id = setTimeout(function () {callback(currTime + timeToCall, lastTime);
      }, timeToCall);
      lastTime = currTime + timeToCall;
      return id;
    };
const cancelAnimationFrame = function (id: any) {clearTimeout(id);
    };

而后咱们就能够去动态变化进度条啦。

好啦 到这里咱们的 taro 适配也就实现啦,让咱们来看一看成果吧:

gif 图可能不太显著,大家能够微信搜寻 NutUI 小程序尝试一下。

结语

本文介绍了 NutUI 中 circleProgress 组件的设计思路与实现原理,与大家共勉。最初再提一下咱们的 NutUI 组件库,长期以来,团队的小伙伴都在尽心尽力地保护着 NutUI。在之后的日子里,这种保持也不会放弃,咱们仍然会踊跃地保护与迭代,为有须要的同学提供技术支持,也会不定时地公布一些相干的文章帮忙大家更好地了解与应用咱们的组件库。

来点个 Star ❤️ 反对咱们一下吧 ~

退出移动版