一、背景
本文讲解如何用taro-react封装一个圆形进度条组件,重难点在技术实现选型以及如何用svg适配H5及微信小程序
二、效果图
三、参数设计
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
percent | 百分比 | number | 0 |
size | 圆环直径,单位为px | number | 100 |
color | 进度条颜色 | string | #DC8D20 |
layerColor | 轨道背景颜色 | string | #EFEFEF |
text | 文字 | string | - |
strokeWidth | 进度条宽度,单位为px | number | 4 |
四、技术实现
常见实现方案:
- canvas
- svg
(1)canvas
我首先尝试用canvas实现,taro提供了canvas API,画出来之后,发现小程序是没有问题的,但是H5控制台直接报错 仔细翻看文档后,发现canvas API果然不支持H5
(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标签,一个是背景色,一个是进度条色,如下图
height
,width
均用props-size(圆环直径)
;那么圆心的x,y坐标
就均是size / 2
,这很好理解半径r
这里需要注意,等于(size - strokeWidth * 2) / 2
,也就是(直径-边框宽度*2) / 2
,而非直径 / 2
- 圆的填充色
fill
均是none,因为是空心圆 strokeWidth
边框宽度直接用props-strokeWidth
即可stroke
,边框填充色,背景圆用props-layerColor
,上面的圆用props-color
以上属性画好了两个圆,一个是背景色的圆,一个是进度条圆,背景圆整个圆周长都填充,进度条圆,需要根据百分比填充,如何实现呢,利用strokeDasharray
、strokeDashoffset
进行偏移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是没问题的,而微信小程序出不来,微信小程序控制台报错如下
于是,我又仔细的去翻看文档,查到微信小程序目前不支持 SVG 标签,仅仅支持加载 SVG 之后 作为 background-image 进行展示,如 background-image: url("data:image/svg+xml.......)
,或者 base64 后作为 background-image 的 url,可见社区官方文章
于是尝试用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及小程序
但在使用过程中我们发现了适配问题
适配
使用过程中,发现单位没有做适配,一般来说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%);
}
}