效果图
实现思路
代码过多,但不复杂
- 采用Canvas 2D,文档请搜索h5 canvas 开发文档;
- 定义4个canvas,通过相对定位重叠在一起:
<!-- 绘制背景半圆刻度 -->
<canvas type="2d" id="bgLine" style="position:absolute;"></canvas>
<!-- 绘制百分比刻度(带动画) -->
<canvas type="2d" id="line" style="position:absolute;"></canvas>
<!-- 绘制小三角(带动画) -->
<canvas type="2d" id="mark" style="position:absolute;"></canvas>
<!-- 绘制中间文字(带动画) -->
<canvas type="2d" id="text" style="position:absolute;"></canvas>
- 利用cxt.setLineDash([10,10])设置为虚线,画圆弧;
- 通过修改中心点,旋转画布来实现动画;
(1)初始化
let scaleTimmer,markTimmer,textTimmer;
let bgLineCtx,markCtx,textCtx,lineCtx;
Component({
data: {
canvas: {
size: 220,
r: 100, // 半径
progress: 0, // 显示进度 (单位百分比)
index: 0, // 开始刻度
defaultColor: 'rgba(255,255,255,0.25)', // 背景刻度颜色
activeColor: 'rgba(255,255,255,1)', // 进度条颜色
collegeNum: 0, //推荐数量
totle: 0, //推荐总量 2000
time: 1000, //动画所需总时间 1000 = 1s
timer: 20, //延迟间隔 1000 = 1s
textUnit: '个', //文案单位 所/个
text:'自定义文案', //文案
}
},
methods:{
/**
* 组件方法调用入口
*/
loadArcProgress(data) {
const that= this;
this.setData({
'canvas.collegeNum':data.total,
'canvas.totle':data.totleLimit,
'canvas.progress':(data.total / data.totleLimit) * 100
},()=>{
//
if(bgLineCtx && markCtx && textCtx && lineCtx){
that.drawBg();
that.scaleAnimation();
that.markAnimation();
that.textAnimation();
}else{
that.initCtx().then(()=>{
that.drawBg();
that.scaleAnimation();
that.markAnimation();
that.textAnimation();
})
}
})
},
/**
* 初始化 ctx
* 获取dom节点信息
*/
initCtx(){
return new Promise((resolve)=>{
wx.createSelectorQuery().in(this).selectAll('#bgLine,#line,#mark,#text')
.fields({ node: true, size: true })
.exec((res) => {
const dpr = wx.getSystemInfoSync().pixelRatio
// #bgLine
let bgLineCanvas = res[0][0].node
bgLineCtx = bgLineCanvas.getContext('2d')
bgLineCanvas.width = res[0][0].width * dpr
bgLineCanvas.height = res[0][0].height * dpr
bgLineCtx.scale(dpr, dpr)
// #line
let lineCanvas = res[0][1].node
lineCtx = lineCanvas.getContext('2d')
lineCanvas.width = res[0][1].width * dpr
lineCanvas.height = res[0][1].height * dpr
lineCtx.scale(dpr, dpr)
// #mark
let markCanvas = res[0][2].node
markCtx = markCanvas.getContext('2d')
markCanvas.width = res[0][2].width * dpr
markCanvas.height = res[0][2].height * dpr
markCtx.scale(dpr, dpr)
// #text
let textCanvas = res[0][3].node
textCtx = textCanvas.getContext('2d')
textCanvas.width = res[0][3].width * dpr
textCanvas.height = res[0][3].height * dpr
textCtx.scale(dpr, dpr)
resolve()
})
})
}
}
})
(2)绘制背景半圆刻度
drawBg() {
let ctx = bgLineCtx;
let ctx1 = markCtx;
let obj = this.data.canvas;
ctx.clearRect(0, 0, obj.size, obj.size);
ctx1.clearRect(0, 0, obj.size, obj.size);
ctx.beginPath()
ctx.strokeStyle = obj.defaultColor;
ctx.lineWidth=2.5;
ctx.setLineDash([2.5, 2.5]);
ctx.arc(obj.size / 2,obj.size / 2,obj.size / 2-10,Math.PI,2*Math.PI);
ctx.stroke();
},
(3)绘制百分比刻度(带动画)
/**
* 绘制白色刻度进度
* 通过rotate旋转画布,修改中心点
* draw保存上一次绘制内容
*/
scaleAnimation() {
let ctx = lineCtx;
let obj = this.data.canvas;
ctx.beginPath()
ctx.strokeStyle = obj.activeColor;
ctx.lineWidth=2.5;
ctx.setLineDash([2.5, 2.5]);
function draw(x){
ctx.beginPath()
ctx.clearRect(0,0,obj.size,obj.size / 2);
ctx.arc(obj.size / 2,obj.size / 2,obj.size / 2-10,Math.PI,Math.PI+x);
ctx.stroke();
}
let num = obj.progress * (Math.PI / 100); // 转到多少 π
let addNum = num / (obj.time / obj.timer); // 每次转多少 π
function animate(s){
scaleTimmer = setInterval(function () {
s += addNum;
if (s > num) {
draw(num);
clearInterval(scaleTimmer);
} else {
draw(s);
}
}, obj.timer)
}
clearInterval(scaleTimmer);
if(obj.collegeNum>0){
animate(obj.index)
}else{
ctx.clearRect(0,0,obj.size / 2,obj.size / 2);
}
},
(4)绘制小三角(带动画)
/**
* 绘制小三角
* rotate旋转画布,修改中心点
* draw清空上一次内容
*/
markAnimation() {
let ctx = markCtx;
let obj = this.data.canvas;
function draw(x){
ctx.clearRect(0,0,obj.size,obj.size);
ctx.save();
// 角度 = 弧度 * 180 / Math.PI
let deg = (Math.PI/180)*x*180 / Math.PI;
let offsetY = -(Math.sin(deg) * obj.r);
let offsetX = -(Math.cos(deg) * obj.r);
ctx.fillStyle='#fff';
ctx.translate(obj.size / 2 + offsetX, obj.size / 2 + offsetY);
ctx.rotate(deg);
ctx.beginPath();
ctx.moveTo(-11, -4);
ctx.lineTo(-3, 0);
ctx.lineTo(-11, 4);
ctx.closePath();
ctx.fill();
ctx.restore();
}
let num = obj.progress * (Math.PI / 100); // 转到多少 π
let addNum = num / (obj.time / obj.timer); // 每次转多少 π
function animate(s){
markTimmer = setInterval(function () {
s += addNum;
if (s > num) {
draw(num);
clearInterval(markTimmer);
} else {
draw(s);
}
}, obj.timer)
}
clearInterval(markTimmer);
if(obj.collegeNum>0){
animate(obj.index);
}else{
draw(0);
}
},
(5)绘制中间文字(带动画)
/**
* 绘制中间文字
* 默认 000 三位起,当推荐数量4位时 默认 0000 四位起
* draw清空上一次内容
*/
textAnimation() {
let ctx = textCtx;
let obj = this.data.canvas
obj.numFontSize = 12;
obj.textFontSize = 39.6;
ctx.beginPath();
ctx.font = obj.textFontSize+'px Arial';
obj.textY = obj.size / 2 - 25;
let collegeNumLen = obj.collegeNum.toString().length;
if (collegeNumLen == 4) {
obj.textNumW = ctx.measureText('0000').width;
} else {
obj.textNumW = ctx.measureText('000').width;
}
ctx.beginPath();
ctx.font = obj.numFontSize+'px Arial';
obj.textSuoW = ctx.measureText('个').width;
function drawText(num){
ctx.clearRect(0,0,obj.size,obj.size)
ctx.beginPath();
ctx.fillStyle='#fff';
ctx.textAlign='center';
ctx.textBaseline='middle';
ctx.font=obj.numFontSize+'px Arial';
ctx.fillText(obj.text, obj.size / 2, obj.size / 2 - 10);
ctx.beginPath();
ctx.font = obj.textFontSize+'px Arial';
ctx.textBaseline='bottom';
ctx.fillText(num, obj.size / 2 - obj.textSuoW / 2, obj.textY + 9)
ctx.beginPath();
ctx.font = obj.numFontSize+'px Arial';
ctx.textBaseline='bottom'
ctx.fillText(obj.textUnit, obj.size / 2 + obj.textNumW / 2 + 2, obj.textY);
}
let addNum = Math.ceil(obj.collegeNum / (obj.time / obj.timer));
function animate(s){
textTimmer = setInterval(function () {
s = s + addNum;
if (s >= obj.collegeNum) {
let num = obj.collegeNum.toString();
num = num.length == 1 ? `00${num}` : num.length == 2 ? `0${num}` : num;
drawText(num);
clearInterval(textTimmer);
} else {
let num = s.toString();
if (collegeNumLen == 4) {
num = num.length == 1 ? `000${num}` : num.length == 2 ? `00${num}` : num.length == 3 ? `0${num}` : num;
} else {
num = num.length == 1 ? `00${num}`: num.length == 2 ? `0${num}` : num;
}
drawText(num);
}
}, obj.timer)
}
if (collegeNumLen == 4) {
drawText('0000');
} else {
drawText('000');
}
clearInterval(textTimmer);
animate(obj.index);
}
使用组件(组件整体代码在下面)
index.wxml
<arcProgress id="arcProgress"></arcProgress>
index.json
{
"usingComponents": {
"arcProgress":"/components/arcProgress/arcProgress"
}
}
index.js
onLoad(){
this.selectComponent('#arcProgress').loadArcProgress({
total: 1293, //推荐数量
totleLimit: 2000, //推荐总量
});
}
组件整体代码
/components/arcProgress.wxml
<view style="height:{{canvas.size/2}}px;width:{{canvas.size}}px;margin:auto;position:relative;line-height:1;color:#fff;">
<view class="textFS" style="position:absolute;bottom:0;right:{{canvas.size}}px;">0</view>
<view class="textFS" style="position:absolute;bottom:0;left:{{canvas.size}}px;">{{canvas.totle}}</view>
<canvas type="2d" id="bgLine" style="width:{{canvas.size}}px;height:{{canvas.size/2}}px;position:absolute;"></canvas>
<canvas type="2d" id="line" style="width:{{canvas.size}}px;height:{{canvas.size/2}}px;position:absolute;"></canvas>
<canvas type="2d" id="mark" style="width:{{canvas.size+10}}px;height:{{canvas.size/2+20}}px;position:absolute;"></canvas>
<canvas type="2d" id="text" style="width:{{canvas.size}}px;height:{{canvas.size/2}}px;position:absolute;"></canvas>
</view>
/components/arcProgress.js
/**
* 默认 调用组件方法
*
* this.selectComponent('#arcProgress').loadArcProgress({
total: 0, //推荐数量
totleLimit: 0, //推荐总量
});
* 获取到数据后 调用组件方法
*
* this.selectComponent('#arcProgress').loadArcProgress({
total: 1293, //推荐数量
totleLimit: 2000, //推荐总量
});
*/
let scaleTimmer,markTimmer,textTimmer;
let bgLineCtx,markCtx,textCtx,lineCtx;
Component({
data: {
canvas: {
size: 220,
r: 100, // 半径
progress: 0, // 显示进度 (单位百分比)
index: 0, // 开始刻度
defaultColor: 'rgba(255,255,255,0.25)', // 背景刻度颜色
activeColor: 'rgba(255,255,255,1)', // 进度条颜色
collegeNum: 0, //推荐数量
totle: 0, //推荐总量 2000
time: 1000, //动画所需总时间 1000 = 1s
timer: 20, //延迟间隔 1000 = 1s
textUnit: '个', //文案单位 所/个
text:'自定义文案', //文案
}
},
methods: {
/**
* 初始化 ctx
* 获取dom节点信息
*/
initCtx(){
return new Promise((resolve)=>{
wx.createSelectorQuery().in(this).selectAll('#bgLine,#line,#mark,#text')
.fields({ node: true, size: true })
.exec((res) => {
const dpr = wx.getSystemInfoSync().pixelRatio
// #bgLine
let bgLineCanvas = res[0][0].node
bgLineCtx = bgLineCanvas.getContext('2d')
bgLineCanvas.width = res[0][0].width * dpr
bgLineCanvas.height = res[0][0].height * dpr
bgLineCtx.scale(dpr, dpr)
// #line
let lineCanvas = res[0][1].node
lineCtx = lineCanvas.getContext('2d')
lineCanvas.width = res[0][1].width * dpr
lineCanvas.height = res[0][1].height * dpr
lineCtx.scale(dpr, dpr)
// #mark
let markCanvas = res[0][2].node
markCtx = markCanvas.getContext('2d')
markCanvas.width = res[0][2].width * dpr
markCanvas.height = res[0][2].height * dpr
markCtx.scale(dpr, dpr)
// #text
let textCanvas = res[0][3].node
textCtx = textCanvas.getContext('2d')
textCanvas.width = res[0][3].width * dpr
textCanvas.height = res[0][3].height * dpr
textCtx.scale(dpr, dpr)
resolve()
})
})
},
loadArcProgress(data) {
const that= this;
this.setData({
'canvas.collegeNum':data.total,
'canvas.totle':data.totleLimit,
'canvas.progress':(data.total / data.totleLimit) * 100
},()=>{
if(bgLineCtx && markCtx && textCtx && lineCtx){
that.drawBg();
that.scaleAnimation();
that.markAnimation();
that.textAnimation();
}else{
that.initCtx().then(()=>{
that.drawBg();
that.scaleAnimation();
that.markAnimation();
that.textAnimation();
})
}
})
},
drawBg() {
let ctx = bgLineCtx;
let ctx1 = markCtx;
let obj = this.data.canvas;
ctx.clearRect(0, 0, obj.size, obj.size);
ctx1.clearRect(0, 0, obj.size, obj.size);
ctx.beginPath()
ctx.strokeStyle = obj.defaultColor;
ctx.lineWidth=2.5;
ctx.setLineDash([2.5, 2.5]);
ctx.arc(obj.size / 2,obj.size / 2,obj.size / 2-10,Math.PI,2*Math.PI);
ctx.stroke();
},
/**
* 绘制白色刻度进度
* 通过rotate旋转画布,修改中心点
* draw保存上一次绘制内容
*/
scaleAnimation() {
let ctx = lineCtx;
let obj = this.data.canvas;
ctx.beginPath()
ctx.strokeStyle = obj.activeColor;
ctx.lineWidth=2.5;
ctx.setLineDash([2.5, 2.5]);
function draw(x){
ctx.beginPath()
ctx.clearRect(0,0,obj.size,obj.size / 2);
ctx.arc(obj.size / 2,obj.size / 2,obj.size / 2-10,Math.PI,Math.PI+x);
ctx.stroke();
}
let num = obj.progress * (Math.PI / 100); // 转到多少 π
let addNum = num / (obj.time / obj.timer); // 每次转多少 π
function animate(s){
scaleTimmer = setInterval(function () {
s += addNum;
if (s > num) {
draw(num);
clearInterval(scaleTimmer);
} else {
draw(s);
}
}, obj.timer)
}
clearInterval(scaleTimmer);
if(obj.collegeNum>0){
animate(obj.index)
}else{
ctx.clearRect(0,0,obj.size / 2,obj.size / 2);
}
},
/**
* 绘制小三角
* rotate旋转画布,修改中心点
* draw清空上一次内容
*/
markAnimation() {
let ctx = markCtx;
let obj = this.data.canvas;
function draw(x){
ctx.clearRect(0,0,obj.size,obj.size);
ctx.save();
// 角度 = 弧度 * 180 / Math.PI
let deg = (Math.PI/180)*x*180 / Math.PI;
let offsetY = -(Math.sin(deg) * obj.r);
let offsetX = -(Math.cos(deg) * obj.r);
ctx.fillStyle='#fff';
ctx.translate(obj.size / 2 + offsetX, obj.size / 2 + offsetY);
ctx.rotate(deg);
ctx.beginPath();
ctx.moveTo(-11, -4);
ctx.lineTo(-3, 0);
ctx.lineTo(-11, 4);
ctx.closePath();
ctx.fill();
ctx.restore();
}
let num = obj.progress * (Math.PI / 100); // 转到多少 π
let addNum = num / (obj.time / obj.timer); // 每次转多少 π
function animate(s){
markTimmer = setInterval(function () {
s += addNum;
if (s > num) {
draw(num);
clearInterval(markTimmer);
} else {
draw(s);
}
}, obj.timer)
}
clearInterval(markTimmer);
if(obj.collegeNum>0){
animate(obj.index);
}else{
draw(0);
}
},
/**
* 绘制中间文字
* 默认 000 三位起,当推荐数量4位时 默认 0000 四位起
* draw清空上一次内容
*/
textAnimation() {
let ctx = textCtx;
let obj = this.data.canvas
obj.numFontSize = 12;
obj.textFontSize = 39.6;
ctx.beginPath();
ctx.font = obj.textFontSize+'px Arial';
obj.textY = obj.size / 2 - 25;
let collegeNumLen = obj.collegeNum.toString().length;
if (collegeNumLen == 4) {
obj.textNumW = ctx.measureText('0000').width;
} else {
obj.textNumW = ctx.measureText('000').width;
}
ctx.beginPath();
ctx.font = obj.numFontSize+'px Arial';
obj.textSuoW = ctx.measureText('个').width;
function drawText(num){
ctx.clearRect(0,0,obj.size,obj.size)
ctx.beginPath();
ctx.fillStyle='#fff';
ctx.textAlign='center';
ctx.textBaseline='middle';
ctx.font=obj.numFontSize+'px Arial';
ctx.fillText(obj.text, obj.size / 2, obj.size / 2 - 10);
ctx.beginPath();
ctx.font = obj.textFontSize+'px Arial';
ctx.textBaseline='bottom';
ctx.fillText(num, obj.size / 2 - obj.textSuoW / 2, obj.textY + 9)
ctx.beginPath();
ctx.font = obj.numFontSize+'px Arial';
ctx.textBaseline='bottom'
ctx.fillText(obj.textUnit, obj.size / 2 + obj.textNumW / 2 + 2, obj.textY);
}
let addNum = Math.ceil(obj.collegeNum / (obj.time / obj.timer));
function animate(s){
textTimmer = setInterval(function () {
s = s + addNum;
if (s >= obj.collegeNum) {
let num = obj.collegeNum.toString();
num = num.length == 1 ? `00${num}` : num.length == 2 ? `0${num}` : num;
drawText(num);
clearInterval(textTimmer);
} else {
let num = s.toString();
if (collegeNumLen == 4) {
num = num.length == 1 ? `000${num}` : num.length == 2 ? `00${num}` : num.length == 3 ? `0${num}` : num;
} else {
num = num.length == 1 ? `00${num}`: num.length == 2 ? `0${num}` : num;
}
drawText(num);
}
}, obj.timer)
}
if (collegeNumLen == 4) {
drawText('0000');
} else {
drawText('000');
}
clearInterval(textTimmer);
animate(obj.index);
}
}
})