大致的效果是这样
废话多说 直接看代码 HTML
<view class="progress-container">
<canvas class="canvas" type="2d" id="myCanvas" style="width: 750rpx;height: 460rpx;"></canvas>
</view>
JS
需要两个参数,已完成的数量和总量。
totalCount: {
type: Number, //总数
value: 0,
},
completedNum:{
type: Number, //完成的数量
value: 0,
}
在组件的attached生命周期中定义初始值
attached() {
this.value = 0 ; //完成数量的起始值
this.endValue = 0; //未完成的初始值,如果不需要可将此部分的代码逻辑删除
this.r = 80 ; //圆的半径
this.init(); //执行canvas
},
OK 然后开始创建画布了 采用了canvas2d的类型,由于画这个仪表盘的时候写了挺多的demo 自我感觉2d比webgl清晰且更加流畅
init() {
const query = this.createSelectorQuery();
const {totalCount,completedNum} = this.data;
query
.select('#myCanvas')
.fields({
node: true,
size: true
})
.exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const system = wx.getSystemInfoSync();
const dpr = system.pixelRatio;
const ratio = system.windowWidth / 375; //计算屏幕比列 更好的适配不同机型
canvas.width = res[0].width * dpr;//canvas狂傲
canvas.height = res[0].height * dpr;
ctx.scale(dpr, dpr); //同比缩放
ctx.translate(system.windowWidth/2,230*ratio/2) //将圆心偏移到画布中心,230是canvas的高度 这样更好计算往后的数据
// 设置圆环的宽度
ctx.lineWidth = parseInt(12 * ratio);
this.drawBottomColor(ctx, ratio);
let renderLoop = null;
//设置动画执行 主要使用了小程序canvas的requestAnimationFrame
//此处ifelse的判断是已完成的数量多还是未完成的数量多,要执行数量比较多那部分的逻辑,否则动画执行不完全 如果不需要未完成部分 ,无需执行此判断。
if(completedNum>=totalCount/2){
renderLoop = ()=>{
if (this.value <= this.data.completedNum) {
this.render(ctx, ratio);
canvas.requestAnimationFrame(renderLoop);
if(this.endValue >= totalCount-completedNum){ //如果数量已经超过本身未完成的数量,就让他一直等于这个数值。
this.endValue = totalCount-completedNum
}
this.drawEndExtendLine(ctx, ratio,this.endValue++) //每次执行的时候value ++ 下方代码逻辑同理
}
}
}else{
renderLoop = () =>{
if(this.endValue<=totalCount-completedNum){
if(this.value>=completedNum){
this.value = completedNum
}
this.render(ctx, ratio);
this.drawEndExtendLine(ctx, ratio,this.endValue++)
canvas.requestAnimationFrame(renderLoop);
}
}
}
canvas.requestAnimationFrame(renderLoop);
});
},
执行render函数
render(ctx, ratio) {
ctx.clearRect(-375/2*ratio, -230/2*ratio, 375 * ratio, 230 * ratio); //清除画布
this.draw(ctx, ratio);//开始画图
},
draw(ctx, ratio) {
// 画底色
this.drawBottomColor(ctx, ratio);
// 画高亮色
this.drawActiveColor(ctx, ratio, this.value++);
ctx.stroke();
},
首先执行的是底部那个淡灰色的圆弧和圆弧内部的虚线
drawBottomColor(ctx, ratio) {
let startAngle = 120/ 180 * Math.PI ; //开始角度 这样更容易计算出角度
const endAngle = 60/ 180 * Math.PI ; //结束角度 总角度是300
ctx.beginPath();
ctx.lineCap = 'round' //线条接口处形状
ctx.lineWidth = 12*ratio //宽度
ctx.strokeStyle = '#71aefe';
ctx.arc(0,0,this.r*ratio,startAngle,endAngle);
ctx.stroke(); //填充底部灰色圆弧
ctx.beginPath() //开始绘制底部虚线
ctx.lineCap = 'butt' //线条接口处形状
ctx.strokeStyle = 'rgba(255,255,255,0.5)'
ctx.lineWidth = 3 * ratio;
// ctx.lineDashOffset = 5;
ctx.setLineDash([2, 2.5]);
ctx.arc(0, 0, this.r*ratio- 15, startAngle, endAngle) //圆弧的半径 - 15 位置可按照自己的需求计算
ctx.stroke()
},
然后开始画高亮的圆弧
// 高亮进度条
drawActiveColor(ctx, ratio,nowValue) {
const {totalCount} = this.data ; 和上面的灰色圆弧的不同的就是结束位置,需要计算一下
let startAngle = 120 * Math.PI / 180; //开始角度
const finishedValue = nowValue / totalCount * 300 //已完成部分结束角度 300总角度
const finishedAngle = (120 + finishedValue) * Math.PI / 180
ctx.beginPath();
if(this.data.completedNum>0){
ctx.lineCap = 'round' //线条接口处形状
}
ctx.lineWidth = 12*ratio //宽度
ctx.strokeStyle = '#fff'; // 设置圆环的颜色
ctx.arc(0,0,this.r * ratio,startAngle,finishedAngle);
ctx.stroke();
this.drawCenterFont(ctx, ratio,nowValue) //绘制中心文字
this.drawExtendLine(ctx, ratio,nowValue) //延长线
},
开始写中间的文字比较简单,唯一需要注意的一点就是需要获取一下文字的宽度,避免1位数2位数3位数的时候覆盖到别的文字,或者距离又太远
drawCenterFont(ctx, ratio,nowValue){
ctx.font = "36px normal";
ctx.textAlign = 'center'
ctx.fillStyle = '#fff'
const textWidth = ctx.measureText(this.data.totalCount); //获取文字宽度
ctx.fillText(this.data.totalCount, 0, 0)
ctx.font = "14px normal";
ctx.fillText('个', textWidth.width / 2 + 10, 0)
ctx.fillText('任务', 0, 22 * ratio)
},
然后到了最麻烦的一个部分,如果没有延长线的需求到这个地方就可以结束了。
//延长线
drawExtendLine(ctx,ratio,nowValue){
const {totalCount} = this.data ;
const extendLineValue = nowValue/totalCount*300
// 我们首先要确定延长线的各个地方的位置,然后再去根据三角函数去计算x,y的位置。这样的好处就是延长线可以在圆弧的不同位置有不同的展示,避免了延长线和圆弧有重叠 如果不想用这个方法,可以固定写死
//这个函数计算出位置
const postionArr = this.computedPosition(extendLineValue,ratio,totalCount,nowValue)
ctx.beginPath()
ctx.lineWidth = 2*ratio //宽度
ctx.fillStyle = '#fff'
ctx.setLineDash([]); //将线条切回实线
ctx.arc(postionArr[0].x,postionArr[0].y,3,0,2*Math.PI) //先画延长线顶端的小圆球
ctx.stroke();
ctx.beginPath()
ctx.lineCap = 'butt' //线条接口处形状
ctx.lineWidth = 2*ratio
ctx.setLineDash([3, 2]);
ctx.moveTo(postionArr[1].x ,postionArr[1].y) //然后开始画延长线
ctx.lineTo(postionArr[2].x,postionArr[2].y)
ctx.lineTo(postionArr[3].x,postionArr[3].y)
ctx.stroke();
ctx.font = "18px normal";
//文字的位置计算的不是太准确,如果有需求可以自行计算一下
ctx.fillText(Math.round(nowValue/totalCount*100),postionArr[4].x - 3,postionArr[4].y - 5)
ctx.font = "12px normal"
ctx.fillText("%", postionArr[5].x -7*ratio,postionArr[5].y - 5*ratio)
ctx.fillText("已完成", postionArr[4].x +5*ratio, postionArr[4].y + 12*ratio)
},
//计算已完成延长线各点xy的位置
computedPosition(extendLineValue,ratio,totalCount,nowValue){
//各个角度的位置 可自信更改
const angleArr = [
{ x:92, y:92 },
{ x:95, y:95 },
{ x:105, y:105 },
{ x:135, y: 105 },
{ x:160, y: 110 },
{ x:135, y: 110 },
];
let postionArr = [];
//这个延长线如果超过一半的位置 就让他停下 所以计算这部分
if(nowValue>=parseInt(totalCount * 0.4)){
for(let item of angleArr){
postionArr.push({
x :Math.cos((120 + totalCount/totalCount*300* 0.2) * Math.PI / 180) * (item.x * ratio),
y: Math.sin((120 + totalCount/totalCount*300* 0.2) * Math.PI / 180) * (item.y * ratio)
})
}
}else{
for(let item of angleArr){
postionArr.push({
x :Math.cos((120 + extendLineValue / 2 ) * Math.PI / 180) * (item.x * ratio),
y: Math.sin((120 + extendLineValue / 2 ) * Math.PI / 180) * (item.y * ratio)
})
}
}
return postionArr
},
未完成的线同理,这部分有很大的优化空间,毕竟重复的代码有点多。
//尾部的延长线
drawEndExtendLine(ctx,ratio,endvalue){
const {totalCount} = this.data;
const unFinishdValue = (endvalue)/totalCount*300;
const postionArr = this.computedEndPosition(unFinishdValue,ratio,endvalue,totalCount)
ctx.beginPath();
ctx.setLineDash([]); //将线条切回实线
ctx.arc(postionArr[0].x,postionArr[0].y,3*ratio,0,2*Math.PI,true)
ctx.stroke();
ctx.beginPath()
ctx.setLineDash([3, 2]);
ctx.moveTo(postionArr[1].x ,postionArr[1].y)
ctx.lineTo(postionArr[2].x,postionArr[2].y)
ctx.lineTo(postionArr[3].x,postionArr[3].y)
ctx.stroke();
ctx.font = "18px normal";
ctx.fillText(Math.round(endvalue/totalCount*100),postionArr[4].x ,postionArr[4].y - 5)
ctx.font = "12px normal"
ctx.fillText("%", postionArr[5].x ,postionArr[5].y - 5*ratio)
ctx.fillText("未完成", postionArr[4].x + 5*ratio, postionArr[4].y + 12*ratio)
},
//计算尾部延长线各x,y的位置
computedEndPosition(unFinishdValue,ratio,endValue,totalCount){
const angleArr = [
{ x:92, y:92 },
{ x:95, y:95 },
{ x:105, y:105 },
{ x:135, y: 105 },
{ x:150, y: 100 },
{ x:170, y: 100 },
];
let postionArr = [];
if(endValue>parseInt(totalCount * 0.4)){
for(let item of angleArr){
postionArr.push({
x:Math.cos((60-totalCount/totalCount*300* 0.2) * Math.PI /180) * (item.x* ratio),
y: Math.sin((60-totalCount/totalCount*300* 0.2) * Math.PI/180) * (item.y*ratio)
})
}
}else{
for(let item of angleArr){
postionArr.push({
x:Math.cos((60-unFinishdValue/2) * Math.PI /180) * (item.x* ratio),
y: Math.sin((60-unFinishdValue/2) * Math.PI/180) * (item.y*ratio)
})
}
}
return postionArr
}
到这里这个仪表盘就算完成了,第一次写canvas写的不是很好。请各位大佬轻喷...