做微信小程序的朋友大都接触过或自己动手写过自定义组件,最近我一直都在忙微信小程序的项目,这里我分享一个项目中自己写的一个环形进度条。
废话不多说,先上效果图:
是不是你想要的效果呢?下面说一下这么个东东是咋整出来的吧!
先看下项目结构
ProgressView文件夹里面就是环形进度条的主要代码了!
先说下思路。整这么个东东,毫无疑问肯定得用到画布,画的东西也不复杂,画一个底环,上面覆盖一个当前进度的弧形就行了,至于中间的文字用css定位居中就好了。思路有了,那么,盘它~
创建自定义组件
先看看布局代码:
ProgressView.wxml
<view class="canvasView">
<canvas canvas-id="circleBar" style="width:{{width}}rpx;height:{{height}}rpx;">
</canvas>
<text hidden="{{!showText}}" style="font-size:{{textSize}}rpx;color:{{textColor}}">{{percentage}}%</text>
</view>
ProgressView.wxss
.canvasView{
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
text{
position: absolute;
}
布局很简单,一个view里面放一个canvas组件和一个text组件就好了,canvas组件用来绘制环形进度条,text组件用来展示中间的进度百分比文字。然后可以看到画布的宽高都是引用了属性,并且通过属性控制了是否显示中间的文字,及文字大小颜色,这些怎么操作后面再说。
接下来就开始在画布上面绘制进度条了。这里有个问题就是,画布的相关API的单位全都是px,为了屏幕适配我们应该先把单位转换一下:
var windWidth = wx.getSystemInfoSync().windowWidth;//屏幕宽度
//由于canvas的单位的px,为了适配所有屏幕,这里之前所有像素单位赋值之前都要换成以rpx为单位的大小乘以xs
const xs = windWidth / 750;
这里首先获取屏幕的宽度windWidth ,windWidth 单位是px,然后用windWidth/750计算出每个物理单位占用了多少像素,之后用户就可以直接按照UI设计图上的大小填写rpx为单位的数值,我们把每个数值都乘以xs在赋值给画布的API就好了。至于这里windWidth为什么是除以750?是因为小程序框架规定屏幕宽就是750rpx。不信可以去试试,无论什么分辨率手机,你把一个组件的宽度设为750rpx他它正好是一个屏幕的宽度。这里就不多说,网上有很多解释,有兴趣取百度一下。
接下来我们在ProgressView.js文件中先添加几个属性:
/**
* 组件的属性列表
*/
properties: {
//画布的宽度 单位rpx
canvasWidth: {
type: Number,
value: 400
},
//线条宽度 默认16,单位rpx
lineWidth: {
type: Number,
value: 16
},
//线条颜色 默认"#E3AF6A"
lineColor: {
type: String,
value: "#E3AF6A"
},
//进度条底色
bottomColor: {
type: String,
value: "#FFF9F1"
},
//当前的值
value: {
type: Number,
value: 36
},
//最大值 默认100
maxValue: {
type: Number,
value: 100
},
//是否显示中间进度值文字
showText: {
type: Boolean,
value: true
},
//中间字体大小,单位rpx
textSize: {
type: Number,
value: 60
},
//中间字体颜色
textColor: {
type: String,
value: "#E3AF6A"
}
}
属性也不少,具体用来干嘛的我就不细说了,注释写的很清楚。然后写代码开始绘制图形了。
一步步来,首先我们绘制一个底环,在ProgressView.js文件中添加绘制圆环的方法drawProgressBar:
/**
* 绘制环形进度条
*/
drawProgressBar(){
var canvasWidth = this.data.canvasWidth
//设置画布宽高
this.setData({
width: canvasWidth,
height: canvasWidth
})
//作画
var circle_r = canvasWidth * xs / 2; //画布的一半,用来找中心点和半径
var bottomColor = this.data.bottomColor //进度条底色
var lineColor = this.data.lineColor; //线条颜色
var lineWidth = this.data.lineWidth * xs; //线条宽度
//获取canvas 的绘图上下文
var ctx = this.data.ctx
if (!ctx) {
ctx = wx.createCanvasContext("circleBar", this); //创建 canvas 的绘图上下文 CanvasContext 对象
this.setData({
ctx: ctx
})
}
ctx.translate(circle_r, circle_r);//改变坐标原点的位置,将原点当做圆心作画
//绘制底色圆弧
ctx.beginPath();//开始创建一个路径
ctx.setStrokeStyle(bottomColor);//设置描边颜色
ctx.setLineWidth(lineWidth);//设置线条的宽度
ctx.arc(0, 0, circle_r-lineWidth/2, 0, Math.PI*2, false);//创建一条弧线
ctx.setLineCap('round') //末端圆弧
ctx.stroke();//画出当前路径的边框
ctx.closePath();//关闭一个路径
//计算中间进度文字的值
var maxValue = this.data.maxValue; //最大值
var value = this.data.value; //当前的值
//更新进度值
this.setData({
percentage: parseInt(value / maxValue * 100)
})
ctx.draw(); //清空上次内容绘制本次内容
}
上面的代码用来绘制底环,基本上每一句代码我都写了注释,大家应该是能够看懂的,唯一要说一下的就是Math.PI,这个Math.PI是是个什么意思,这个只在这里用来设置绘制的弧度大小,大家看下图
我这里设置Math.PI*2其实就是代表绘制整个圆的路径,还有上面用到的API大家不了解的可以去翻翻官方文档。
这段代码绘制出来的效果图如下:
好,这样我们的底环也就绘制完了,接下来绘制当前进度的圆环就好了,有了绘制底环的经验,我们依葫芦画瓢加上一段绘制当前进度圆环的代码,修改drawProgressBar方法:
/**
* 绘制环形进度条
*/
drawProgressBar() {
var canvasWidth = this.data.canvasWidth
//设置画布宽高
this.setData({
width: canvasWidth,
height: canvasWidth
})
//作画
var circle_r = canvasWidth * xs / 2; //画布的一半,用来找中心点和半径
var bottomColor = this.data.bottomColor //进度条底色
var lineColor = this.data.lineColor; //线条颜色
var lineWidth = this.data.lineWidth * xs; //线条宽度
//获取canvas 的绘图上下文
var ctx = this.data.ctx
if (!ctx) {
ctx = wx.createCanvasContext("circleBar", this); //创建 canvas 的绘图上下文 CanvasContext 对象
this.setData({
ctx: ctx
})
}
ctx.translate(circle_r, circle_r);//改变坐标原点的位置,将原点当做圆心作画
//绘制底色圆弧
ctx.beginPath();//开始创建一个路径
ctx.setStrokeStyle(bottomColor);//设置描边颜色
ctx.setLineWidth(lineWidth);//设置线条的宽度
ctx.arc(0, 0, circle_r - lineWidth / 2, 0, Math.PI * 2, false);//创建一条弧线
ctx.setLineCap('round') //末端圆弧
ctx.stroke();//画出当前路径的边框
ctx.closePath();//关闭一个路径
//计算中间进度文字的值
var maxValue = this.data.maxValue; //最大值
var value = this.data.value; //当前的值
//更新进度值
this.setData({
percentage: parseInt(value / maxValue * 100)
})
//计算当前进度弧形大小,环形总长度为Math.PI*2
var percent = (Math.PI * 2) * (value / maxValue)
//当前进度的圆弧
ctx.beginPath();//开始创建一个路径
ctx.setStrokeStyle(lineColor);//设置描边颜色
ctx.setLineWidth(lineWidth);//设置线条的宽度
ctx.arc(0, 0, circle_r - lineWidth / 2, -0.5 * Math.PI, percent - 0.5 * Math.PI, false);//创建一条弧线
ctx.setLineCap('round') //末端圆弧
ctx.stroke();//画出当前路径的边框
ctx.closePath();//关闭一个路径
ctx.draw(); //清空上次内容绘制本次内容
}
加上了计算当前进度的弧度大小,并绘制当前进度的代码,效果图如下:
好了基本的东西我们已经全部整完了,可以满足部分人的需求了,但有些同学需要的不是整个圆的,而是半个圆或者2/3个圆的进度条,怎么办?有了上面的经验,其实我们只需要改变绘制圆环的弧度大小就好了,但我这里不想写死,给他设置一个属性,用户自己设置需要绘制多大的圆弧,这里我们添加一个属性radian,最小值为0,最大值为1,代表绘制一个圆的弧度也就是360°
//进度条的弧度大小,最小值为0,最大值为1,默认为1代表绘制一个圆的弧度也就是360°
radian:{
type: Number,
value: 1
}
看看修改后的代码:
/**
* 绘制环形进度条
* @param value 当前进度值
* @param maxValue 进度最大值
*/
drawProgressBar(value, maxValue) {
var radian=this.data.radian//弧度大小
radian=radian>1?1:radian //控制最大值为1
radian=radian<0?0:radian //控制最小值为0
//作画
var bottomColor = this.data.bottomColor //进度条底色
var lineColor = this.data.lineColor; //线条颜色
var lineWidth = this.data.lineWidth * xs; //线条宽度
var canvasWidth = this.data.canvasWidth//画布的宽
var canvasHeight=(Math.cos((1-radian)*Math.PI)+1)*canvasWidth/2+lineWidth //计算画布的高度
//设置画布宽高,高最低是进度框的半径
this.setData({
width: canvasWidth,
height: radian<0.5?canvasWidth/2:canvasHeight
})
//计算弧形的起点,这是为了保证起点和终点在同一水平线上
var startPath=Math.PI*(3/2-radian)
var endPath=startPath+2*Math.PI*radian
//获取canvas 的绘图上下文
var ctx = this.data.ctx
if (!ctx) {
ctx = wx.createCanvasContext("circleBar", this); //创建 canvas 的绘图上下文 CanvasContext 对象
this.setData({
ctx: ctx
})
}
var circle_r = canvasWidth * xs / 2; //画布的一半,用来找中心点和半径
ctx.translate(circle_r, circle_r);//改变坐标原点的位置,将原点当做圆心作画
//绘制底色圆弧
ctx.beginPath();//开始创建一个路径
ctx.setStrokeStyle(bottomColor);//设置描边颜色
ctx.setLineWidth(lineWidth);//设置线条的宽度
ctx.arc(0, 0, circle_r - lineWidth / 2, startPath, endPath, false);//创建一条弧线
ctx.setLineCap('round') //末端圆弧
ctx.stroke();//画出当前路径的边框
ctx.closePath();//关闭一个路径
//计算中间进度文字的值
var maxValue = maxValue!=null?maxValue:this.data.maxValue; //最大值
var value =value!=null?value: this.data.value; //当前的值
//更新进度值
this.setData({
percentage: parseInt(value / maxValue * 100)
})
//计算当前进度弧形大小,环形总长度为Math.PI*2
var percent = 2*Math.PI*radian* (value / maxValue)
//当前进度的圆弧
ctx.beginPath();//开始创建一个路径
ctx.setStrokeStyle(lineColor);//设置描边颜色
ctx.setLineWidth(lineWidth);//设置线条的宽度
ctx.arc(0, 0, circle_r - lineWidth / 2, startPath, percent +startPath, false);//创建一条弧线
ctx.setLineCap('round') //末端圆弧
ctx.stroke();//画出当前路径的边框
ctx.closePath();//关闭一个路径
ctx.draw(); //清空上次内容绘制本次内容
}
上面的代码我修改了几处地方,加了两个参数,
* @param value 当前值
* @param maxValue 最大值
考虑到很多进度最大值并不是100,也许是其他数值,所以这里加了两个参数,maxValue传你进度的最大值(不传默认是100),value传进度的当前值,通过这两个参数计算出进度值的百分比显示出来。
还一个是添加了通过自定义的radian属性(最小值为0,最大值为1,默认为1代表绘制一个圆的弧度也就是360°)绘制不同弧度大小的圆弧。这里最关键的是怎么就计算出圆弧的起始点和结束点:
//计算弧形的起点,这是为了保证起点和终点在同一水平线上
var startPath=Math.PI*(3/2-radian) //起始点
var endPath=startPath+2*Math.PI*radian //结束点
这个起始点和结束点怎么来的大家画个图自己算一下也就出来了,不复杂。
起始点一个半圆的大小就是PI,知道了radian属性值就可以计算出起始点了,Math.PI - Math.PI*(radian-1/2)就是起始点,知道了起始点,起始点加上总进度大小就是结束点了startPath+2*Math.PI*radian。说的可能有点抽象,自己拿笔画个图算一下也不难。
使用
在需要使用进进度条界面的json文件引用组件
{
"usingComponents": {
"progressView": "/static/component/ProgressView/ProgressView"
}
}
在wxml界面加上组件
<progressView id="progressView2" canvasWidth="500" radian="{{3/4}}"/>
<view class="buttonView">
<button bindtap="minus">-10</button>
<button bindtap="plus">+10</button>
</view>
我这里设置了组件宽度为500rpx,进度框弧度大小为3/4个圆的大小,其他属性都用默认的并加了两个按钮加进度和减进度。
页面的js代码:
index.js
var value=0
Page({
data: {
},
/**
* 加
*/
plus:function(){
if(value<100){
value+=10
//this.progressView.showCanvasRing(value, 100); //绘制环形进度
this.progressView2.drawProgressBar(value, 100); //绘制环形进度
}
},
/**
* 减
*/
minus: function() {
if (value >0) {
value-=10
//this.progressView.showCanvasRing(value, 100); //绘制环形进度
this.progressView2.drawProgressBar(value, 100); //绘制环形进度
}
},
onLoad: function() {
//this.progressView = this.selectComponent("#progressView");
this.progressView2 = this.selectComponent("#progressView2");
//this.progressView.showCanvasRing(value, 100); //绘制环形进度
this.progressView2.drawProgressBar(value, 100); //绘制环形进度
},
})
写了加进度和减进度的方法,我们看看效果:
效果还行,可用。
但这里还有个小问题,当用户设置属性radian为1,也就是进度条是整个圆时,那起始点的方向只能从六点钟开始,如图:
这样的话还不太友好,我们再加个属性,用来控制当前进度值的起始点位置。
优化
修改后的js代码
ProgressView.js
var app = getApp();
var windWidth = wx.getSystemInfoSync().windowWidth;//屏幕宽度
//由于canvas的单位的px,为了适配所有屏幕,这里之前所有像素单位赋值之前都要换成以rpx为单位的大小乘以xs
const xs = windWidth / 750;
Component({
/**
* 组件的属性列表
*/
properties: {
//画布的宽度 单位rpx
canvasWidth: {
type: Number,
value: 400
},
//进度条的弧度大小,最小值为0,最大值为1,默认为1代表绘制一个圆的弧度也就是360°
radian: {
type: Number,
value: 1 / 2
},
//线条宽度 默认16,单位rpx
lineWidth: {
type: Number,
value: 16
},
//线条颜色 默认"#E3AF6A"
lineColor: {
type: String,
value: "#E3AF6A"
},
//进度条底色
bottomColor: {
type: String,
value: "#FFF9F1"
},
//当前的值
value: {
type: Number,
value: 36
},
//最大值 默认100
maxValue: {
type: Number,
value: 100
},
//当前进度值的方向,只在radian为1时起作用,可填left(九点钟方向开始),top(12点钟方向开始),bottom(6点钟方向开始),right(3点钟方向开)
direction: {
type: String,
value: "top"
},
//是否显示中间进度值文字
showText: {
type: Boolean,
value: true
},
//中间字体大小,单位rpx
textSize: {
type: Number,
value: 60
},
//中间字体颜色
textColor: {
type: String,
value: "#E3AF6A"
}
},
/**
* 组件的初始数据
*/
data: {
ctx: null,
width: 400,
height: 400,
percentage: 0 //中间百分比的值
},
/**
* 组件的方法列表
*/
methods: {
/**
* 绘制环形进度条
* @param value 当前进度值
* @param maxValue 进度最大值
*/
drawProgressBar(value, maxValue) {
var radian = this.data.radian//弧度大小
radian=radian>1?1:radian //控制最大值为1
radian=radian<0?0:radian //控制最小值为0
//作画
var bottomColor = this.data.bottomColor //进度条底色
var lineColor = this.data.lineColor; //线条颜色
var lineWidth = this.data.lineWidth * xs; //线条宽度
var canvasWidth = this.data.canvasWidth//画布的宽
var canvasHeight = (Math.cos((1 - radian) * Math.PI) + 1) * canvasWidth / 2 + lineWidth //计算画布的高度
//设置画布宽高,高最低是进度框的半径
this.setData({
width: canvasWidth,
height: radian < 0.5 ? canvasWidth / 2 : canvasHeight
})
//计算弧形的起点,这是为了保证起点和终点在同一水平线上
var startPath = Math.PI * (3 / 2 - radian)
var endPath = startPath + 2 * Math.PI * radian
//获取canvas 的绘图上下文
var ctx = this.data.ctx
if (!ctx) {
ctx = wx.createCanvasContext("circleBar", this); //创建 canvas 的绘图上下文 CanvasContext 对象
this.setData({
ctx: ctx
})
}
var circle_r = canvasWidth * xs / 2; //画布的一半,用来找中心点和半径
ctx.translate(circle_r, circle_r);//改变坐标原点的位置,将原点当做圆心作画
//绘制底色圆弧
ctx.beginPath();//开始创建一个路径
ctx.setStrokeStyle(bottomColor);//设置描边颜色
ctx.setLineWidth(lineWidth);//设置线条的宽度
ctx.arc(0, 0, circle_r - lineWidth / 2, startPath, endPath, false);//创建一条弧线
ctx.setLineCap('round') //末端圆弧
ctx.stroke();//画出当前路径的边框
ctx.closePath();//关闭一个路径
//计算中间进度文字的值
var maxValue = maxValue != null ? maxValue : this.data.maxValue; //最大值
var value = value != null ? value : this.data.value; //当前的值
//更新进度值
this.setData({
percentage: parseInt(value / maxValue * 100)
})
//计算当前进度弧形大小,环形总长度为Math.PI*2
var percent = 2 * Math.PI * radian * (value / maxValue)
var currStartPath = startPath //当前进度值的起点位置
//如果radian为1则使用自定义的起始位置
if (radian == 1) {
var direction = this.data.direction//当前进度值
switch (direction) {
case 'left':
currStartPath =Math.PI //九点钟方向
break
case 'top':
currStartPath = 3 / 2 * Math.PI //十二点钟方向
break
case 'right':
currStartPath = 0 //三点钟方向
break
case 'bottom':
currStartPath = 1 / 2 * Math.PI //六点钟方向
break
}
}
//当前进度的圆弧
ctx.beginPath();//开始创建一个路径
ctx.setStrokeStyle(lineColor);//设置描边颜色
ctx.setLineWidth(lineWidth);//设置线条的宽度
ctx.arc(0, 0, circle_r - lineWidth / 2, currStartPath, percent + currStartPath, false);//创建一条弧线
ctx.setLineCap('round') //末端圆弧
ctx.stroke();//画出当前路径的边框
ctx.closePath();//关闭一个路径
ctx.draw(); //清空上次内容绘制本次内容
}
}
})
这里增加了direction属性控制起点方向,并控制该属性只在radian为1时起作用,比如我想要将进度条设置成圆环,并且起点方向从九点钟方向开始,则在组件设置radian为1,direction为 left 就好了:
<progressView id="progressView2" canvasWidth="500" radian="1" direction="left"/>
效果图:
好了,自定义一个基本的环形进度条大致就是这样了,应该能满足一些需求了,有些人可能还需要更加个性化一点,由于不同的场景有不同的需求,我这里没法一一实现,大家可以下载源码在上面进行调整。
源码地址: