本次分享是在实际业务中遇到的一个需求,如图下:
实现一个圆形进度条 要求展示三段颜色,和渐变颜色 在日常业务中,我们遇到了一些需求,需要实现一个 圆形进度条,并且该进度条需要展示 三段颜色,且颜色之间要 渐变过渡。最终的效果如下图所示:
需求细节:
• 进度条为圆形
• 进度条的颜色分为三段,第一段是前20%的绿色,第二段都需要平滑渐变,第三段是背景色
• 使用 小程序Canvas 实现
- 创建一个canvas对象 绑定一个id
<canvas id="progressCanvas" type="2d" style="width: 340rpx; height: 320rpx;"></canvas>
Canvas 元素将用于绘制圆形进度条。我们使用微信小程序 wx.createSelectorQuery() 获取 Canvas 的节点,并通过设置 pixelRatio 以适配高分屏设备。
2.这是我们定义的js文件,
- 接收父组件传递 percent 表示进度条的百分比。默认为
0 - 在生命周期 ready 中,dom节点渲染完后 来进行canvas的初始化操作
- initCanvas 里做了:获取dom元素 画布尺寸,并设置他的宽高和 dpr(适应高分辨率屏幕)
- 接下来在 drawProgressCircle 函数里面进行绘画
``
Component({
properties: {
percent: {
type: Number,
value: 0,
},
},
lifetimes: {
ready() {
this.initCanvas();
},
},
data: {
canvas: null,
ctx: null,
canvasWidth: 200, // 设置一个默认宽度
canvasHeight: 200, // 设置一个默认高度
},
methods: {
// 初始化 Canvas
initCanvas() {
const query = wx.createSelectorQuery().in(this);
query
.select('#progressCanvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const dpr = wx.getSystemInfoSync().pixelRatio;
// 获取 Canvas 的宽高并适配高分屏
const canvasWidth = res[0].width * dpr;
const canvasHeight = res[0].height * dpr;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
ctx.scale(dpr, dpr);
this.setData({
canvas,
ctx,
canvasWidth: res[0].width,
canvasHeight: res[0].height,
});
// 初次绘制进度条
this.drawProgressCircle(this.properties.percent || 0);
});
},
},
});
- 定义一些常量
-
FULL_CIRCLE_ANGLE:表示完整的圆形角度,2 * Math.PI(360 度)。 -
lineWidth:圆环的宽度,14 像素。 -
radius:圆的半径,70 像素。 -
START_ANGLE_OFFSET:起始角度的偏移量,用于确定环形进度条的开始绘制位置。这里的0.745 * Math.PI大约是135度,使进度条从该角度开始绘制。 -
zoomSize:百分比的缩放基准值,用于计算进度条的终点角度。这里设为 132(可以根据自己需求来调)。
// 绘制环形进度条
drawProgressCircle(percent) {
const ctx = this.data.ctx;
if (!ctx) return;
const FULL_CIRCLE_ANGLE = 2 * Math.PI; // 完整角度
const lineWidth = 14; // 圆环的宽度
const radius = 70; // 圆的半径
// 获取 canvas 的中心坐标
const centerX = this.data.canvasWidth / 2;
const centerY = this.data.canvasHeight / 2;
const START_ANGLE_OFFSET = 0.745 * Math.PI; // 起始角度偏移量
const zoomSize = 132; // percent 的缩放基准 满足拱形
// 计算实际进度(percent)条终点角度
const progressEndAngle = START_ANGLE_OFFSET + FULL_CIRCLE_ANGLE * (percent / zoomSize);
}
- 绘制背景色 ctx.arc(centerX, centerY, radius, START_ANGLE_OFFSET, 0.25 * Math.PI, false)
- (centerX, centerY)代表圆心的位置
- radius 圆的半径
- START_ANGLE_OFFSET 开始角度
- 0.25 * Math.PI 结束角度
- false 顺时针绘画
// 首先清空画布
ctx.clearRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);
// 绘制背景圆环
ctx.lineWidth = lineWidth;
ctx.strokeStyle = '#CADFDF';
ctx.lineCap = 'round';
ctx.beginPath();
ctx.arc(centerX, centerY, radius, START_ANGLE_OFFSET, 0.25 * Math.PI, false);
ctx.stroke();
- 绘制渐变色,这里咱们先绘制渐变色是因为 前20%绿色的层级要比渐变色高 ,注意canvas的层叠
// 计算渐变段数和角度
this.drawArcWithGradient(
ctx,
centerX,
centerY,
radius,
START_ANGLE_OFFSET,
progressEndAngle,
percent,
);
下面这些函数 放在methods里 和
// 分段渐变绘制函数
drawArcWithGradient(
ctx,
centerX,
centerY,
radius,
startAngle,
endAngle,
segments,
) {
const segmentAngle = (endAngle - startAngle) / segments; // 每段的角度
const startColor = '#b4d477'; // 起始颜色
const middleColor = '#efcd2d'; // 中间颜色
const endColor = '#e84033'; // 结束颜色
const colors = this.generateGradientColors(
startColor,
middleColor,
endColor,
80,
);
for (let i = 0; i < segments; i++) {
const segmentStart = startAngle + i * segmentAngle;
const segmentEnd = segmentStart + segmentAngle;
// 计算每一段的渐变颜色
const progress = i / segments;
const colorIndex = Math.floor(progress * (colors.length - 1));
const nextColorIndex = colorIndex + 1;
// 插值计算颜色过渡
const ratio = (progress * (colors.length - 1)) % 1;
const color = this.interpolateColor(
colors[colorIndex],
colors[nextColorIndex],
ratio,
);
// 设置线的颜色
ctx.strokeStyle = color;
// 开始绘制
ctx.beginPath();
ctx.arc(centerX, centerY, radius, segmentStart, segmentEnd, false);
ctx.stroke();
}
},
// 颜色插值函数,用于过渡
interpolateColor(color1, color2, ratio) {
const hex = (x) => x.toString(16).padStart(2, '0');
const r = Math.ceil(
parseInt(color1.substring(1, 3), 16) * (1 - ratio) +
parseInt(color2.substring(1, 3), 16) * ratio,
);
const g = Math.ceil(
parseInt(color1.substring(3, 5), 16) * (1 - ratio) +
parseInt(color2.substring(3, 5), 16) * ratio,
);
const b = Math.ceil(
parseInt(color1.substring(5, 7), 16) * (1 - ratio) +
parseInt(color2.substring(5, 7), 16) * ratio,
);
return `#${hex(r)}${hex(g)}${hex(b)}`;
},
// console.log(gradientColors);
generateGradientColors(startColor, middleColor, endColor, numColors) {
const colors = [];
const midPoint = Math.floor(numColors / 2);
// 插值从起始颜色到中间颜色
for (let i = 0; i < midPoint; i++) {
const ratio = i / (midPoint - 1);
colors.push(this.interpolateColor(startColor, middleColor, ratio));
}
// 插值从中间颜色到结束颜色
for (let i = 0; i < numColors - midPoint; i++) {
const ratio = i / (numColors - midPoint - 1);
colors.push(this.interpolateColor(middleColor, endColor, ratio));
}
return colors;
},
- 绘制绿色部分
- 先绘制一条与背景色同色的北京 ,当整体进度超过20的时候展示
// 绘制白色的进度条(作为底部边框效果)
const whitePercent = percent >= 20 ? 20.7 / zoomSize : 0;
const whiteEndAngle = START_ANGLE_OFFSET + FULL_CIRCLE_ANGLE * whitePercent;
ctx.strokeStyle = '#CADFDF';
ctx.lineCap = 'round';
ctx.beginPath();
ctx.arc(centerX, centerY, radius, START_ANGLE_OFFSET, whiteEndAngle, false);
ctx.stroke();
// 绿色进度条(显示前 20%)
const greenPercent = percent > 20 ? 20 / zoomSize : percent / zoomSize;
const greenEndAngle = START_ANGLE_OFFSET + FULL_CIRCLE_ANGLE * greenPercent;
ctx.strokeStyle = '#5cc57f';
ctx.lineCap = 'round';
ctx.beginPath();
ctx.arc(centerX, centerY, radius, START_ANGLE_OFFSET, greenEndAngle, false);
ctx.stroke();
- 点缀小白点(不知道该叫他什么) 只是个进度装饰 没有功能
- 这里 开始角度和结束角度一定不要相同,在手机上会出现 不生效(不显示)的情况
- progressEndAngle - 0.001
// 白点
ctx.lineWidth = lineWidth - 5;
ctx.strokeStyle = '#FFF';
ctx.beginPath();
ctx.arc(centerX, centerY, radius, progressEndAngle - 0.001, progressEndAngle, false);
ctx.stroke();
- 白色背景
// 绘制白色的大圆点背景底色
ctx.lineWidth = 106;
ctx.strokeStyle = '#FFF';
ctx.fillStyle = '#FFF';
ctx.beginPath();
ctx.arc(centerX, centerY, 10, 0, FULL_CIRCLE_ANGLE, false);
ctx.fill();
ctx.stroke();
// 灰色点缀线
ctx.lineWidth = 0.5;
ctx.strokeStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath();
ctx.arc(centerX, centerY, 56, 0, FULL_CIRCLE_ANGLE, false);
ctx.stroke();
- 绘制文字
// 绘制文字 80%
ctx.textAlign = 'center';
ctx.font = '22px Arial';
ctx.fillStyle = 'rgba(0,0,0,1)';
ctx.fillText(`${percent.toFixed(1)}%`, centerX, centerY + 15);
// 绘制辅助文字
ctx.font = '10px Arial';
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillText('已用费用比', centerX, centerY - 15);