https://harmonyos.51cto.com
前言
圆环进度条组件在开发过程中是经常会用到的组件,比如在loading加载、升级过程,下载过程等等都需要用到。本文是基于HarmonyOS JSPAI开发,使用canvas画布封装的组件。方便直接引入上手使用。
效果展示
属性
对应组件传入的prop参数
props: {
// 圆环宽度
cWidth: {
type: Number,
default: 200,
},
// 圆环高度
cHeight: {
type: Number,
default: 200,
},
// 缩放级,值越大,越清晰,渲染压力越大
staticScale:{
type: Number,
default: 4,
},
// 显示文本
text: {
type: String,
default: '',
},
// 圆环宽度
lineWidth: {
type: Number,
default: 20
},
// 当前进度
cRate: {
type: Number,
default: 0
},
// 进度颜色,当只有一个颜色值时为纯色进度,最多只支持两个颜色,使用半角逗号分开
cColor: {
type: String,
default: '#86C1FF,#2B5BF9'
},
// 背景色
bgColor:{
type: String,
default: '#e9ebed'
},
// 文字大小
textSize: {
type: Number,
default: 32
}
},
调用组件
<element name="khCircle" src="../../../share/component/khCircle/khCircle"></element>
<div class="container">
<khCircle
c-width="{{cWidth}}"
c-weight="{{cHeight}}"
line-width="{{lineWidth}}"
c-rate="{{cRate}}"
static-scale="{{staticScale}}"
c-color="{{cColor}}"
bg-color="{{bgColor}}"
text="{{cRate + '%'}}"
text-size="40"
></khCircle>
</div>
实现原理
我们知道,在canvas中可以实现线性渐变和径向渐变,但是这些渐变都不够美观并且无法实现根据圆环方向线性渐变。
本文通过将圆环弧切片成若干等分绘制指定的颜色来实现的。
下面拆分各个步骤
- 创建canvas对象并且设置canvas大小
- 使用canvas的arc() API绘制一个圆充当圆环背景,给lineWidth设置圆环宽度的值
- 计算两个颜色之间的渐变颜色值。确定需要切片的精度,来确定渐变颜色值之间的差级。
- 在原来的背景圆环上,再绘制一个圆弧,圆弧是根据开始位置和结束位置,然后绘制一段圆弧之间的所有切片
1. 初始化canvas
这里需要有个关键点,getContext(contextType, contextAttributes)方法可以传入两个参数
- contextType 上下文类型 可选 2d、webgl
- contextAttributes 上下文属性
注意:在HarmonyOS 的JSPAI官网文档没有提到这两个属性,但是案例上有看到const ctx = el.getContext('2d', { antialias: true });这样一句话。这里似乎跟webAPI的有些区别。
antialias属性是设置是否开启抗锯齿,但是在webAPI中,只有在上下文类型为webgl的时候才有这个属性。
先写个canvas标签
<canvas id="canvas" class="canvas" ref="canvas"></canvas>
再初始化canvas对象
initCanvas(){
let canvas = this.$refs.canvas;
this.ctx = canvas.getContext('2d');
canvas.width = this.cWidth * this.staticScale;
canvas.height = this.cHeight * this.staticScale;
},
2. 绘制背景圆环
通过canvas的arc(x,y,r,sAngle,eAngle,counterclockwise)方法绘制一个圆
参数值
counterclockwise可选。规定应该逆时针还是顺时针绘图。False = 顺时针,true = 逆时针。
在canvas绘制圆的其实位置是在x+r的位置开始,完整的圆弧为2PI。因为我们要在最顶端开始绘制,所以我们的开始位置是1.5PI,而我们的结束位置就不是2PI了,而是1.5PI + 2 * PI,这个才是绘制的完整的一个圆弧。最后
this.ctx.beginPath();
this.ctx.lineCap = "round"; //向线条的每个末端添加圆形线帽
this.ctx.lineWidth = 20; //绘制圆的边框
this.ctx.strokeStyle = '#ff9800'; //绘制边框的颜色
this.ctx.arc(100,100,100,Math.PI * 1.5,(Math.PI * 1.5 * Math.PI * 2));
this.ctx.stroke();
上面代码我们这里绘制了一个圆心坐标在(100,100),半径为100,边框为20,边框颜色为 #ff9800的圆环
3. 实现圆弧切片绘制方法
3.1. 获取两颜色之间的渐变值组
- 首先我们需要把获取到的hex色值转换成rgb三原色值,
- 然后通过计算开始颜色和结束颜色总差值来获取每一步的颜色值,
- 最后再转换成hex设置
将hex色值转换成rgb三原色值方法
// 将hex表示方式转换为rgb表示方式(这里返回rgb数组模式)
hexToRgb(sColor){
var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
sColor = sColor.toLowerCase();
if(sColor && reg.test(sColor)){
if(sColor.length === 4){
var sColorNew = "#";
for(var i=1; i<4; i+=1){
sColorNew += sColor.slice(i,i+1).concat(sColor.slice(i,i+1));
}
sColor = sColorNew;
}
//处理六位的颜色值
var sColorChange = [];
for(let i=1; i<7; i+=2){
sColorChange.push(parseInt("0x"+sColor.slice(i,i+2)));
}
return sColorChange;
}else{
return sColor;
}
},
通过计算开始颜色和结束颜色总差值来获取每一步的颜色值
/**
* @description: 封装颜色渐变之间值
* @param {String} startColor 开始颜色hex
* @param {Number} endColor 结束颜色hex
* @param {Number} step 渐变精度
* @return {Array}
*/
gradientColor(startColor,endColor,step){
let startRGB = this.hexToRgb(startColor);//转换为rgb数组模式
let endRGB = this.hexToRgb(endColor);
let sR = (endRGB[0]-startRGB[0])/step;//总差值
let sG = (endRGB[1]-startRGB[1])/step;
let sB = (endRGB[2]-startRGB[2])/step;
var colorArr = [];
for(var i=0;i<step;i++){
//计算每一步的hex值
var hex = this.rgbToHex('rgb('+parseInt((sR*i+startRGB[0]))+','+parseInt((sG*i+startRGB[1]))+','+parseInt((sB*i+startRGB[2]))+')');
colorArr.push(hex);
}
return colorArr;
},
将rgb三原色值转换成hex色值方法
// 将rgb表示方式转换为hex表示方式
rgbToHex(rgb){
var _this = rgb;
var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
if(/^(rgb|RGB)/.test(_this)){
var aColor = _this.replace(/(?:(|)|rgb|RGB)*/g,"").split(",");
var strHex = "#";
for(var i=0; i<aColor.length; i++){
var hex = Number(aColor[i]).toString(16);
hex = hex<10 ? 0+''+hex :hex;// 保证每个rgb的值为2位
if(hex === "0"){
hex += hex;
}
strHex += hex;
}
if(strHex.length !== 7){
strHex = _this;
}
return strHex;
}else if(reg.test(_this)){
var aNum = _this.replace(/#/,"").split("");
if(aNum.length === 6){
return _this;
}else if(aNum.length === 3){
var numHex = "#";
for(let i=0; i<aNum.length; i+=1){
numHex += (aNum[i]+aNum[i]);
}
return numHex;
}
}else{
return _this;
}
}
3.2. 开始绘制一段圆弧之前的渐变值
这里有个渐变精度unit,这个值确定了我们渐变之间的缓和度,比如一段100像素的长方形,我们每一个像素绘制一个颜色,和每10个像素绘制一个颜色,效果是不一样的。
因为我们需要绘制的整个圆弧是2 * PI ≈ 6.28318;那么就是说在开始颜色到结束颜色之间需要绘制长达6.28318的长度,这里设置unit = 0.01;就是每0.01个圆弧里绘制一个颜色。这样大约需要绘制6.28318 / 0.01 ≈ 628个渐变颜色就可以了,把两个颜色之间色值拆分成628个色值组,会让渐变更缓和,当然还可以调整更多,但是这样也会导致渲染压力正大,耗时,耗性能,而且在手机上其实也看不出更细的区别来。
每次绘制的时候,需要记录当前上一次绘制的结束位置,作为下一次绘制的开始位置,这样子就不需要每次绘制都从头开始,也可以拼接起来。
/**
* @description: 圆弧切片绘制
* @param {Number} percent 圆环展示的进度(0~100
* @param {String} startColor 开始颜色
* @param {String} endColor 结束颜色
*/
paint(percent,startColor,endColor){
percent = percent / 100;
!endColor ? endColor = startColor : '';
// 渐变精度,值越小,渐变越缓和
const unit = 0.01;
let division = parseInt((Math.PI * 2)/unit);
// 生成渐变色数组
let gradient = this.gradientColor(startColor,endColor,division);
let arr = gradient.slice(this.num, parseInt(percent*gradient.length));
this.num = parseInt(percent*gradient.length);
for(let i=0; i < arr.length; i++){
this.endAngle = this.startAngle+unit;
this.drawCircle(arr[i],this.startAngle,this.endAngle);
this.startAngle+=unit;
}
},
最后附上hml、css代码
hml文件
<div class="kh-circle">
<div class="circle-box" style="width: {{cWidth}}px; height: {{cHeight}}px;">
<canvas id="canvas" class="canvas" ref="canvas"></canvas>
<text class="text" show="{{text ? true : false}}" style="font-size: {{textSize}}px;">
{{text}}
</text>
</div>
</div>
css文件
.circle-box{
position: relative;
}
.canvas{
width: 100%;
height: 100%;
}
.text{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
z-index: 10;
text-align: center;
}
最后再来看一下效果:
总结
在开发过程中也遇到了一些坑,比如SDK版本问题:在SDK version7的时候渲染没有问题,但是IDE预览器经常崩溃掉。后来改回SDK6就好了。
以往在开发vue的时候,会直接把canvas对象赋值到data函数里,方便其他方法直接调用,但是在这里却发现不行。最后发现这里data并非是一个函数,而是一个对象,这里还没有深入研究。
在使用组件的时候未必一定需要使用text属性,可以使用slot插槽实现多样文本显示。