5
头图
This article mainly introduces the design and implementation principle of the circleProgress component based on NutUI Vue3. It is a circular progress bar component, which is used to display the current progress of the operation, and supports modification progress and gradient colors. A circular progress bar is a very common component, especially on pages that manage background statistics or tasks that require users to wait.

achieve effect

The effect is as follows

image

Realize ideas

First of all, let's take care of our needs. We need a circular progress bar that can change the progress, has animation, and can support gradient colors .

There are currently three common implementations:

  • CSS implementation
  • SVG implementation
  • Canvas implementation

And SVG is divided into two kinds, one is to directly use circle to realize, and the other is to use path to draw.

Then let's take a look at the implementation of some well-known domestic component libraries:

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

Antd Design and TDesign, varlet is implemented by svg Circle, but the editor personally thinks that the beginning and end of the gradient color of Antd Design is problematic, TDesign and varlet temporarily do not support the linear gradient of the circular progress bar
Element UI and Vant are implemented using svg Path. Vant supports linear gradients, and there is no problem of incompatibility between the beginning and end of gradient colors. Element does not support linear gradients temporarily.

At present, the circular progress bars of the mainstream component libraries are basically drawn with svg, because the implementation idea is simple, using SVG to draw two circles, one circle as the background color, and the other circle as the progress display, there are few problems when using it. NutUI also chose to use SVG to implement progress bars.

The two implementation methods are described in detail below.

circle implementation

First of all, let's talk about SVG circle (the implementation of Antd, Tdesign) , and we will also introduce the linear gradient problem of Antd's progress bar .

First, let's draw the simplest circle first.

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

image

I will not introduce more about the above attributes. Students who don’t understand can actually read the general meaning. r is the radius, cx and cy are the position of the dot, and the width of the color and radian.
Some students here may ask if this is not a circle it can be achieved, but don't forget the effect we need,

Then let's draw a circle that is not 100% progress. How to draw it is used here stroke-dasharray attribute, which can make the stroke of the graph dotted, what needs to be understood here is that the dotted point , its size can be set, it is not really just that one, it can be longer or shorter.
So if the length of the point of circle is exactly equal to the length of the side of circle , then the point looks like the side of circle .
We can calculate the circumference of the ring. The parameter is the arc length, the maximum value, the maximum value is the circumference, and the arc length is the progress value.
image
Everyone should have found the problem. In this case, when the progress is not 100%, some of the radians have no color, so we also need a background color. That is another one 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>

Since here we need it to start from the middle on the left, we also need to add the rotation.
iamge

As for the next step, it's very simple to make it move, so how to move it? Dynamically change the value of stroke-dasharray 6772f47a12b9a89c376d1955d943512b---, and we will talk about how to change it when we introduce path below. Let’s talk about the pits we encountered, that is, when we were making gradients, we would find that our gradients did not begin to gradient from where our progress started.
Here we can also take a look at Antd 's circular progress bar gradient, I use a red to black to show everyone. ( Tdesign here, the editor tried it in the online editor, and the linear gradient did not take effect.)
image

Here, the editor personally thinks that this gradient of Antd is also wrong (it only represents personal thoughts), of course, if you have other ideas, you can also bring them up for discussion.

In fact, it is because the linear gradient is from left to right, and the reason for the rotation of the ring is added to the top. You can search for the solution by yourself, and I will not elaborate here.

path implementation

The following mainly introduces the use of path ( Vant, Element implementation method ) to achieve it, which can simply and perfectly solve the problem that the gradient color above does not correspond ( Element does not support linear gradients temporarily ).

And circle the realization idea is the same, draw two circles, one is used to represent the background color, the other is used to represent the radian, mainly how do we draw a circle?

  1. Learn about the viewBox attribute and add this attribute to the SVG tag. This attribute is used to set the size of the canvas, but please note that it is a relative size that will dynamically adapt to changes in our parent element. For example, we set its attribute to viewBox="0,0,100,100" , in fact, it divides the width and height of our entire canvas into 100 parts, and the SVG elements are displayed on the divided canvas.

    We don't need to pay attention to the width and height of SVG anymore. It has now achieved self-adaptation and will automatically adapt to the width and height of the outer parent element. We give the user a Props in the outermost layer to set the size of the circular progress bar.

     <div :style="{ height: radius * 2 + 'px', width: radius * 2 + 'px' }">
        <svg viewBox="0 0 100 100"></svg>
     </div>
  2. Understand the d attribute of path , since we use path to draw a circle, then of course we have to be familiar with the d attribute, it can draw various lines Come. d attributes are used to define path data. Let's first understand the parameters we need to use:

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

    Please note ⚠️ that these parameters are case sensitive, when it is an uppercase command it indicates that its parameter is an absolute position, and a lowercase command indicates its point of rubbing ambiguous relative to the current position. We can also use negative values as arguments to our commands. Negative relative x values will be shifted to the left, and negative relative y values will be shifted up.

    Let's explain our two parameters in detail below.
    moveto其实很好理解,画笔的指定位置, xy轴, M (绝对) m (relative)

    elliptical Arc The elliptical arc is recorded as follows: Command: A (absolute) a (relative)

    The parameter form of the ellipse arc: (rx ry x-axis- rotation large-arc-flag sweep-flag x y) Detailed parameters: rx ry is the length of the two semi-axes of the ellipse. x-axis-rotation is the rotation angle of the ellipse relative to the coordinate system, in degrees rather than radians. large-arc-flag is the mark to draw the large arc (1) or the small arc (0) part. sweep-flag is whether the marker is drawn clockwise (1) or counterclockwise (0). x y are the coordinates of the arc end point.

    Because we need to give the user the control of the drawing direction at the beginning of the circle, here we accept a props to control the drawing direction. According to the above description, we can write the path d attribute of ---2961a2a0318fc0cbecf1f4781ddba91b---,
    At the absolute position of 50, 50 (the center of the circle), first draw from the position of 45 above the center of the circle (45 is the radius) and then the parameters of the elliptical arc, rx , ry , the radius is 45, the rotation angle is 0, draw a large arc, then the drawing direction, clockwise or counterclockwise, and finally the coordinates of the end point of the arc. The following code can be obtained

     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`;
    });

    In this case, a circle will be drawn, and the other attributes used are the same as the ones above cirlce

stroke : stroke color

stroke-width : stroke width

fill : fill color

stroke-dasharray : How many pixels to draw once

Now that the path of path has been completed, the next step is to color the ring and add dynamic progress changes normally.

First, let's write the background circle at the bottom. Users can customize the color value of the background circle's radian and the width of the arc.

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

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

Next is the ring that displays the progress bar, because we also need a gradient here, so we also need to add some code to the SVG. Check the SVG documentation. We found an SVG element called <linearGradient> , by using This element we can achieve the purpose of color gradient.

  1. Create linearGradient
    Before creating this element, we need to know that <linearGradient> tags must be nested inside <defs> . <defs> tag is definitions的 abbreviation, which defines special elements such as gradients. And we must assign a id attribute to the gradient content, otherwise other elements in the document cannot reference it. In order for the gradient to be reused, the gradient content needs to be defined inside the <defs> tag, not on top of the shape.
  2. Set the color gradient direction Now <linearGradient> the element has been created successfully. Next, we can assign attributes to it to meet the needs of modifying the gradient color direction as required.
    渐变的方向可以通过两个点来控制,它们分别是属性x1x2y1 2d2bfb29f2048101182dbd8e7217e3a2 y2 ,这些属性定义Gradient route direction. The gradient is horizontal by default, but by modifying these properties, it can be rotated.
  3. set gradient color

    In <linearGradient> theoretically there is no upper limit on the colors added, but if you want to have a gradient effect, you need to add at least two colors. So you need to create at least two <stop> elements in <linearGradient> cc307a8cf9990f012b5c457ec6fca2e0--- to add the color attribute you need.
    So we use a loop here to process multiple color properties

    <stop> element has three attributes:

    stop-color : The gradient color you want to set

    offset : On the direction vector you define, define the effective position of the color, and use the percentage to set the specific existing position.

    stop-opacity : set stop-color the transparency of the color (not used temporarily)

     <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;
     });

    After adding the gradient color, let's deal with the progress bar (how to bind the progress bar to this gradient color), it's actually very simple, change the ring stroke to <linearGradient> The only one id is enough; then process the progress display of the ring, which is the same as the processing method above circle , use 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`
    };
});

The stroke-linecap property above is a presentation property that defines the shape to use at the end of an open subpath when it is stroked, and can be used in style .
As for how the 283 above came from, it is actually very simple that the circumference of our ring, 2πr=2 3.1415926 45.
In fact, at this point, our circular progress bar in the normal h5 environment is done.

SVG under Taro

Because NutUI can be used with Taro to develop WeChat mini-programs, the circular progress bar here certainly has the same function in the mini-program environment.

Because the SVG used in our ordinary h5 environment is implemented, we want to apply a set of codes, but it turns out that the use of SVG is not supported in the applet environment.
image

This is the answer in the official documentation of the applet, so I use it as a background image to display it here.
我们通过一些转换SVG为base64的网站发现, <其实是%3C>%3E# -Replace it with # %23 that's fine, because we still have some variables in it, so I will split them up and write them into several variables.

 <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%'
      };
    });

Do you think this is done? No, no, you will find that when you dynamically change the progress of the circular progress bar, there is no animation and it looks rigid and rigid

image

So we will add an animation effect to it here, we will use setTimeout instead of requestAnimationFrame (Those who don't know can understand this attribute, it's very useful!), because Not supported in the applet environment.

 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);
    };

Then we can dynamically change the progress bar.

Well, here our taro adaptation is complete, let's take a look at the effect:

image

The gif image may not be obvious, you can try it by searching for the NutUI applet on WeChat.

Epilogue

This article introduces the design ideas and implementation principles of components in NutUI circleProgress , and share with you. Finally, let's mention our NutUI component library. For a long time, the team's small partners have been dedicated to maintaining NutUI. In the days to come, this persistence will not give up. We will still actively maintain and iterate, provide technical support for students in need, and will also publish some related articles from time to time to help you better understand and use Our component library.

Come and support us with a Star ❤️~


京东设计中心JDC
696 声望1k 粉丝

致力为京东零售消费者提供完美的购物体验。以京东零售体验设计为核心,为京东集团各业务条线提供设计支持, 包括线上基础产品体验设计、营销活动体验设计、品牌创意设计、新媒体传播设计、内外部系统产品设计、企...