【Tard】手把手教你封装一个圆形进度条组件

2,056 阅读5分钟

一、背景

本文讲解如何用taro-react封装一个圆形进度条组件,重难点在技术实现选型以及如何用svg适配H5及微信小程序

二、效果图

image.png

三、参数设计

参数说明类型默认值
percent百分比number0
size圆环直径,单位为pxnumber100
color进度条颜色string#DC8D20
layerColor轨道背景颜色string#EFEFEF
text文字string-
strokeWidth进度条宽度,单位为pxnumber4

四、技术实现

常见实现方案:

  • canvas
  • svg

(1)canvas

我首先尝试用canvas实现,taro提供了canvas API,画出来之后,发现小程序是没有问题的,但是H5控制台直接报错 image.png 仔细翻看文档后,发现canvas API果然不支持H5 image.png

(2)svg

于是我想到了svg,简单的熟悉了下svg之后,我尝试拿svg画图,代码实现如下

circle标签画圆,首先解释下标签的基本属性

  • svg
    • height:svg元素的高
    • width:svg元素的宽
    • viewBox:
      • 前两个参数是对svg元素做位移使用,通常设置为0;
      • 后两个参数表示svg元素可容纳的大小,通常设置为宽高
  • circle
    • cx:圆心的x坐标位置
    • cy:圆心的y坐标位置
    • r: 表示半径
    • fill: 圆的填充色
    • stroke-width: 边框宽度
    • stroke:边框填充色
    • stroke-dasharray:控制画笔的虚实,通过实线和虚线的来控制画
    • stroke-dashoffset:相对于绘制的起点偏移的量
    • stroke-linecap:在开放子路径被设置描边的情况下,用于开放自路径两端的形状

可以看到有两个circle标签,一个是背景色,一个是进度条色,如下图

image.png

  • heightwidth均用props-size(圆环直径);那么圆心的x,y坐标就均是size / 2,这很好理解
  • 半径r这里需要注意,等于(size - strokeWidth * 2) / 2,也就是(直径-边框宽度*2) / 2,而非直径 / 2
  • 圆的填充色fill均是none,因为是空心圆
  • strokeWidth边框宽度直接用props-strokeWidth即可
  • stroke,边框填充色,背景圆用props-layerColor,上面的圆用props-color 以上属性画好了两个圆,一个是背景色的圆,一个是进度条圆,背景圆整个圆周长都填充,进度条圆,需要根据百分比填充,如何实现呢,利用strokeDasharraystrokeDashoffset进行偏移
  • strokeDasharray设为圆周长2*π*r,也就是2 * Math.PI * r
  • strokeDashoffset偏移圆周长 * (1 - percent / 100),比如percent=25,偏移圆周长*0.75的长度

props具体含义在二、参数设计中已说明

import { View } from '@tarojs/components'

render() {
const { percent, size, strokeWidth, layerColor, color, text } = this.props
const r = (size - strokeWidth * 2) / 2
const circumference = 2 * Math.PI * r
return (
    <View>
        <svg height={size} width={size} viewBox={`0 0 ${size} ${size}`}>
            <circle id="circleBg"
              cx={size / 2}
              cy={size / 2}
              r={r}
              fill="none"
              strokeWidth={strokeWidth}
              stroke={layerColor}
            />
            <circle id="circle"
              style={{ transform: 'rotate(-90deg)', transformOrigin: ' 50% 50%' }}
              cx={size / 2}
              cy={size / 2}
              r={r}
              fill="none"
              strokeWidth={strokeWidth}
              stroke={color}
              strokeDasharray={circumference}
              strokeDashoffset={circumference * (1 - percent / 100)}
              strokeLinecap='round'
            />
        </svg>
    </View>
  )
}

画完之后,尴尬的事情来了,H5是没问题的,而微信小程序出不来,微信小程序控制台报错如下 image.png

于是,我又仔细的去翻看文档,查到微信小程序目前不支持 SVG 标签,仅仅支持加载 SVG 之后 作为 background-image 进行展示,如 background-image: url("data:image/svg+xml.......),或者 base64 后作为 background-image 的 url,可见社区官方文章

image.png

于是尝试用background-image: url("")的方式尝试兼容小程序

静态的svg可以直接在自动转换的网站上转换成base64,由于这里的svg是有变量的,所以我们手动转换,查到<替换成%3C>替换成%3E;但是怎么也出不来,最后在这个网站与自动替换静态svg的对比,发现是颜色值的#也需要相应的替换成%23,替换了就可以了!!!代码如下

private transColor = (color) => {
    return color.replace('#', '%23')
}
render() {
     const style: React.CSSProperties = {
      // backgroundImage 这一句在掘金编辑器高亮有问题,所以先注释掉,其实是有用的哈
      // backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='${size}' width='${size}' viewBox='0 0 ${size} ${size}'%3E%3Ccircle id='circleBg' cx='${size / 2}' cy='${size / 2}' r='${r}' fill='none' stroke-width='${strokeWidth}' stroke='${this.transColor(layerColor)}'%3E%3C/circle%3E%3Ccircle id='circle' cx='${size / 2}' cy='${size / 2}' r='${r}' fill='none' stroke-width='${strokeWidth}' stroke='${this.transColor(color)}' stroke-dasharray='${circumference}' stroke-dashoffset='${circumference * (1 - percent / 100)}' stroke-linecap='round' style='transform: rotate(-90deg); transform-origin: 50%25 50%25;'%3E%3C/circle%3E%3C/svg%3E")`,
      width: size + 'px',
      height: size + 'px'
    }
    return(
        <view
            style={style}
        >
        </view>
    )
}

到此也算是封装完成了,用svg的方式兼容了H5及小程序

image.png 但在使用过程中我们发现了适配问题

适配

使用过程中,发现单位没有做适配,一般来说H5用单位rem,小程序用rpx
讲过调查,svg不支持rpx单位,降级h5和小程序都转为rem的话,小程序暂时的实际长度要比预期的短
于是小程序适配为px 在长度处用如下函数包一层

private pxTransformRem = (size: number) => {
    if (!size) return ''
    return size / 750 * Taro.getSystemInfoSync().windowWidth + "px";
}

五、组件基本使用及完整代码

(1)基本使用

基础用法

<ProgressCircle percent={25} text="25%" />

自定义颜色

<ProgressCircle percent={25} color="#FF2929" text="自定义颜色" />

自定义圆环直径

<ProgressCircle percent={25} size={200} text="自定义圆环直径" />

自定义进度条宽度

<ProgressCircle percent={25} size={200} strokeWidth={10} text="自定义进度条宽度" />

(2)完整代码


import React from 'react'
import Taro from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { SlProgressCircleProps } from '../../../types/progress-circle'
import { pxTransform } from '../../common/utils'
import { isWeapp } from '../../common/utils'

export default class SlProgressCircle extends React.Component<SlProgressCircleProps> {
  public static defaultProps: SlProgressCircleProps

  private transColor = (color) => {
    return color.replace('#', '%23')
  }

  private pxTransformRem = (size: number) => {
    if (!size) return ''
    return size / 750 * Taro.getSystemInfoSync().windowWidth + "px";
  }

  public render(): JSX.Element | null {
    const { percent, size, strokeWidth, layerColor, color, text } = this.props
    const r = (size - strokeWidth * 2) / 2
    const circumference = 2 * Math.PI * r
    const svgStyle: React.CSSProperties = {
      height: '100%',
      width: '100%'
    }

    const pxTransformRem = this.pxTransformRem

    const style: React.CSSProperties = {
      // backgroundImage 这一句在掘金编辑器高亮有问题,所以先注释掉,其实是有用的哈
      // backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle id='circleBg' cx='${pxTransformRem(size / 2)}' cy='${pxTransformRem(size / 2)}' r='${pxTransformRem(r)}' fill='none' stroke-width='${pxTransformRem(strokeWidth)}' stroke='${this.transColor(layerColor)}'%3E%3C/circle%3E%3Ccircle id='circle' cx='${pxTransformRem(size / 2)}' cy='${pxTransformRem(size / 2)}' r='${pxTransformRem(r)}' fill='none' stroke-width='${pxTransformRem(strokeWidth)}' stroke='${this.transColor(color)}' stroke-dasharray='${pxTransformRem(circumference)}' stroke-dashoffset='${pxTransformRem(circumference * (1 - percent / 100))}' stroke-linecap='round'%3E%3C/circle%3E%3C/svg%3E")`,
      width: '100%',
      height: '100%',
      transform: 'rotate(-90deg)',
      transformOrigin: '50% 50%'
    }

    return (
      <View className="slc-circle" style={`width:${!isWeapp ? pxTransform(size) : pxTransformRem(size)}; height:${!isWeapp ? pxTransform(size) : pxTransformRem(size)}`}>

        {!isWeapp
          ? <svg style={svgStyle}>
            <circle id="circleBg"
              cx={pxTransform(size / 2)}
              cy={pxTransform(size / 2)}
              r={pxTransform(r)}
              fill="none"
              strokeWidth={pxTransform(strokeWidth)}
              stroke={layerColor}
            />
            <circle id="circle"
              style={{ transform: 'rotate(-90deg)', transformOrigin: '50% 50%' }}
              cx={pxTransform(size / 2)}
              cy={pxTransform(size / 2)}
              r={pxTransform(r)}
              fill="none"
              strokeWidth={pxTransform(strokeWidth)}
              stroke={color}
              strokeDasharray={pxTransform(circumference)}
              strokeDashoffset={pxTransform(circumference * (1 - percent / 100))}
              strokeLinecap='round'
            />
          </svg>
          :
          <view
            style={style}
          >
          </view>
        }
        <Text className="slc-circle-text">{text}</Text>

      </View>
    )
  }
}

SlProgressCircle.defaultProps = {
  percent: 0,
  size: 200,
  strokeWidth: 4,
  layerColor: '#EFEFEF',
  color: '#DC8D20'
}
export interface ProgressCircleProps {
    /**
    * 百分比
    * @default 0
    */
    percent: number
    /**
     * 圆环直径,单位为px
     * @default 100
     */
    size: number
    /**
     * 进度条颜色
     *  @default #DC8D20
     */
    color: string
    /**
     * 轨道背景颜色
     *  @default #EFEFEF
     */
    layerColor: string
    /**
     * 文字
     */
    text?: string
    /**
     * 进度条宽度,单位为px
     * @default 4
     */
    strokeWidth: number
}
.circle {
  position: relative;

  &-text {
    position: absolute;
    width: 100%;
    top: 50%;
    left: 50%;
    text-align: center;
    transform: translate(-50%, -50%);
  }
}