周末好,今天給大家帶來一款接地氣的環形進度條組件vue-awesome-progress
。近日被設計小姐姐要求實現這么一個環形進度條效果,大體由四部分組成,分別是底色圓環,進度弧,環內文字,進度圓點。設計稿截圖如下:
我的第一反應還是找現成的組件,市面上很多組件都實現了前3點,獨獨沒找到能畫進度圓點的組件,不然稍加定制也能復用。既然沒有現成的組件,只有自己用vue + canvas
擼一個了。
效果圖
先放個效果圖,然后再說下具體實現過程,各位看官且聽我慢慢道來。
安裝與使用
源碼地址,歡迎star
和提issue
。
安裝
npm install --save vue-awesome-progress
使用
全局注冊
import Vue from 'vue'
import VueAwesomeProgress from "vue-awesome-progress"
Vue.use(VueAwesomeProgress)
局部使用
import VueAwesomeProgress from "vue-awesome-progress"
export default {
components: {
VueAwesomeProgress
},
// 其他代碼
}
script標簽引入組件
同時也支持直接使用script
標簽引入哦,滿足有這部分需求的朋友。
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script src="path-to/vue-awesome-progress.min.js"></script>
</head>
<body>
<div id="app"></div>
<script>
new Vue({
el: "#app",
template: '<vue-awesome-progress :percentage="40"></vue-awesome-progress>'
})
</script>
</body>
</html>
靜態展示
任何事都不是一蹴而就的,我們首先來實現一個靜態的效果,然后再實現動畫效果,甚至是復雜的控制邏輯。
確定畫布大小
第一步是確定畫布大小。從設計稿我們可以直觀地看到,整個環形進度條的最外圍是由進度圓點確定的,而進度圓點的圓心在圓環圓周上。
因此我們得出偽代碼如下:
// canvasSize: canvas寬度/高度
// outerRadius: 外圍半徑
// pointRadius: 圓點半徑
// pointRadius: 圓環半徑
canvasSize = 2 * outerRadius = 2 * (pointRadius + circleRadius)
據此我們可以定義如下組件屬性:
props: {
circleRadius: {
type: Number,
default: 40
},
pointRadius: {
type: Number,
default: 6
}
},
computed: {
// 外圍半徑
outerRadius() {
return this.circleRadius + this.pointRadius
},
// canvas寬/高
canvasSize() {
return 2 * this.outerRadius + 'px'
}
}
那么canvas
大小也可以先進行綁定了
<template>
<canvas ref="canvasDemo" :width="canvasSize" :height="canvasSize" />
</template>
獲取繪圖上下文
getContext('2d')
方法返回一個用於在canvas
上繪圖的環境,支持一系列2d
繪圖API
。
mounted() {
// 在$nextTick初始化畫布,不然dom還未渲染好
this.$nextTick(() => {
this.initCanvas()
})
},
methods: {
initCanvas() {
var canvas = this.$refs.canvasDemo;
var ctx = canvas.getContext('2d');
}
}
畫底色圓環
完成了上述步驟后,我們就可以着手畫各個元素了。我們先畫圓環,這時我們還要定義兩個屬性,分別是圓環線寬circleWidth
和圓環顏色circleColor
。
circleWidth: {
type: Number,
default: 2
},
circleColor: {
type: String,
default: '#3B77E3'
}
canvas
提供的畫圓弧的方法是ctx.arc()
,需要提供圓心坐標,半徑,起止弧度,是否逆時針等參數。
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
我們知道,Web
網頁中的坐標系是這樣的,從絕對定位的設置上其實就能看出來(top
,left
設置正負值會發生什么變化),而且原點(0, 0)
是在盒子(比如說canvas
)的左上角哦。
對於角度而言,0°
是x
軸正向,默認是順時針方向旋轉。
圓環的圓心就是canvas
的中心,所以x
, y
取outerRadius
的值就可以了。
ctx.strokeStyle = this.circleColor;
ctx.lineWidth = this.circleWidth;
ctx.beginPath();
ctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, 0, this.deg2Arc(360));
ctx.stroke();
注意arc
傳的是弧度參數,而不是我們常理解的360°
這種概念,因此我們需要將我們理解的360°
轉為弧度。
// deg轉弧度
deg2Arc(deg) {
return deg / 180 * Math.PI
}
畫文字
調用fillText
繪制文字,利用canvas.clientWidth / 2
和canvas.clientWidth / 2
取得中點坐標,結合控制文字對齊的兩個屬性textAlign
和textBaseline
,我們可以將文字繪制在畫布中央。文字的值由label
屬性接收,字體大小由fontSize
屬性接收,顏色則取的fontColor
。
if (this.label) {
ctx.font = `${this.fontSize}px Arial,"Microsoft YaHei"`
ctx.fillStyle = this.fontColor;
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(this.label, canvas.clientWidth / 2, canvas.clientWidth / 2);
}
畫進度弧
支持普通顏色和漸變色,withGradient
默認為true
,代表使用漸變色繪制進度弧,漸變方向我默認給的從上到下。如果希望使用普通顏色,withGradient
傳false
即可,並可以通過lineColor
自定義顏色。
if (this.withGradient) {
this.gradient = ctx.createLinearGradient(this.circleRadius, 0, this.circleRadius, this.circleRadius * 2);
this.lineColorStops.forEach(item => {
this.gradient.addColorStop(item.percent, item.color);
});
}
其中lineColorStops
是漸變色的顏色偏移斷點,由父組件傳入,可傳入任意個顏色斷點,格式如下:
colorStops2: [
{ percent: 0, color: '#FF9933' },
{ percent: 1, color: '#FF4949' }
]
畫一條從上到下的進度弧,即270°
到90°
ctx.strokeStyle = this.withGradient ? this.gradient : this.lineColor;
ctx.lineWidth = this.lineWidth;
ctx.beginPath();
ctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, this.deg2Arc(270), this.deg2Arc(90));
ctx.stroke();
其中lineWidth
是弧線的寬度,由父組件傳入
lineWidth: {
type: Number,
default: 8
}
畫進度圓點
最后我們需要把進度圓點補上,我們先寫死一個角度90°
,顯而易見,圓點坐標為(this.outerRadius, this.outerRadius + this.circleRadius)
畫圓點的代碼如下:
ctx.fillStyle = this.pointColor;
ctx.beginPath();
ctx.arc(this.outerRadius, this.outerRadius + this.circleRadius, this.pointRadius, 0, this.deg2Arc(360));
ctx.fill();
其中pointRadius
是圓點的半徑,由父組件傳入:
pointRadius: {
type: Number,
default: 6
}
角度自定義
當然,進度條的角度是靈活定義的,包括開始角度,結束角度,都應該由調用者隨意給出。因此我們再定義一個屬性angleRange
,用於接收起止角度。
angleRange: {
type: Array,
default: function() {
return [270, 90]
}
}
有了這個屬性,我們就可以隨意地畫進度弧和圓點了,哈哈哈哈。
老哥,這種圓點坐標怎么求?
噗......看來高興過早了,最重要的是根據不同角度求得圓點的圓心坐標,這讓我頓時犯了難。
經過冷靜思考,我腦子里閃過了一個利用正余弦公式求坐標的思路,但前提是坐標系原點如果在圓環外接矩形的左上角才好算。仔細想想,冇問題啦,我先給坐標系平移一下,最后求出來結果,再補個平移差值不就行了嘛。
👆畫圖工具不是很熟練,這里圖沒畫好,線歪了,請忽略細節。
好的,我們先給坐標系向右下方平移pointRadius
,最后求得結果再加上pointRadius
就好了。偽代碼如下:
// realx:真實的x坐標
// realy:真實的y坐標
// resultx:平移后求取的x坐標
// resultx:平移后求取的y坐標
// pointRadius 圓點半徑
realx = resultx + pointRadius
realy = resulty + pointRadius
求解坐標的思路大概如下,分四個范圍判斷,得出求解公式,應該還可以化簡,不過我數學太菜了,先這樣吧。
getPositionsByDeg(deg) {
let x = 0;
let y = 0;
if (deg >= 0 && deg <= 90) {
// 0~90度
x = this.circleRadius * (1 + Math.cos(this.deg2Arc(deg)))
y = this.circleRadius * (1 + Math.sin(this.deg2Arc(deg)))
} else if (deg > 90 && deg <= 180) {
// 90~180度
x = this.circleRadius * (1 - Math.cos(this.deg2Arc(180 - deg)))
y = this.circleRadius * (1 + Math.sin(this.deg2Arc(180 - deg)))
} else if (deg > 180 && deg <= 270) {
// 180~270度
x = this.circleRadius * (1 - Math.sin(this.deg2Arc(270 - deg)))
y = this.circleRadius * (1 - Math.cos(this.deg2Arc(270 - deg)))
} else {
// 270~360度
x = this.circleRadius * (1 + Math.cos(this.deg2Arc(360 - deg)))
y = this.circleRadius * (1 - Math.sin(this.deg2Arc(360 - deg)))
}
return { x, y }
}
最后再補上偏移值即可。
const pointPosition = this.getPositionsByDeg(nextDeg);
ctx.arc(pointPosition.x + this.pointRadius, pointPosition.y + this.pointRadius, this.pointRadius, 0, this.deg2Arc(360));
這樣,一個基本的canvas
環形進度條就成型了。
動畫展示
靜態的東西逼格自然是不夠的,因此我們需要再搞點動畫效果裝裝逼。
基礎動畫
我們先簡單實現一個線性的動畫效果。基本思路是把開始角度和結束角度的差值分為N
段,利用window.requestAnimationFrame
依次執行動畫。
比如從30°
到90°
,我給它分為6段,每次畫10°
。要注意canvas
畫這種動畫過程一般是要重復地清空畫布並重繪的,所以第一次我畫的弧線范圍就是30°~40°
,第二次我畫的弧線范圍就是30°~50°
,以此類推......
基本的代碼結構如下,具體代碼請參考vue-awesome-progress v1.1.0
版本,如果順手幫忙點個star
也是極好的。
animateDrawArc(canvas, ctx, startDeg, endDeg, nextDeg, step) {
window.requestAnimationFrame(() => {
// 清空畫布
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
// 求下一個目標角度
nextDeg = this.getTargetDeg(nextDeg || startDeg, endDeg, step);
// 畫圓環
// 畫文字
// 畫進度弧線
// 畫進度圓點
if (nextDeg !== endDeg) {
// 滿足條件繼續調用動畫,否則結束動畫
this.animateDrawArc(canvas, ctx, startDeg, endDeg, nextDeg, step)
}
}
}
緩動效果
線性動畫顯得有點單調,可操作性不大,因此我考慮引入貝塞爾緩動函數easing
,並且支持傳入動畫執行時間周期duration
,增強了可定制性,使用體驗更好。這里不列出實現代碼了,請前往vue-awesome-progress查看。
<vue-awesome-progress label="188人" :duration="10" easing="0,0,1,1" />
<vue-awesome-progress
label="36℃"
circle-color="#FF4949"
:line-color-stops="colorStops"
:angle-range="[60, 180]"
:duration="5"
/>
// 省略部分...
<vue-awesome-progress label="188人" easing="1,0.28,0.17,0.53" :duration="10" />
<vue-awesome-progress
label="36℃"
circle-color="#FF4949"
:line-color-stops="colorStops"
:angle-range="[60, 180]"
:duration="5"
easing="0.17,0.67,0.83,0.67"
/>
可以看到,當傳入不同的動畫周期duration
和緩動參數easing
時,動畫效果各異,完全取決於使用者自己。
其他效果
當然根據組件支持的屬性,我們也可以定制出其他效果,比如不顯示文字,不顯示圓點,弧線線寬與圓環線寬一樣,不使用漸變色,不需要動畫,等等。我們后續也會考慮支持更多能力,比如控制進度,數字動態增長等!具體使用方法,請參考vue-awesome-progress。
更新日志
2019年11月10日更新
由於我從業務場景出發做了這個組件,沒有考慮到大部分場景都是傳百分比控制進度的,因此在v1.4.0
版本做了如下修正:
-
廢棄
angle-range
,改用percentage
控制進度,同時提供start-deg
屬性控制起始角度; -
with-gradient
改為use-gradient
-
通過
show-text
控制是否顯示進度文字 -
支持通過
format
函數自定義顯示文字的規則
結語
寫完這個組件有讓我感覺到,程序員最終不是輸給了代碼和技術的快速迭代,而是輸給了自己的邏輯思維能力和數學功底。就vue-awesome-progress這個組件而言,根據這個思路,我們也能迅速開發出適用於React
,Angular
以及其他框架生態下的組件。工作三年有余,接觸了不少框架和技術,經歷了MVVM
,Hybrid
,小程序
,跨平台
,大前端
,serverless
的大火,也時常感慨“學不動了”,在這個快速演進的代碼世界里常常感到失落。好在自己還沒有丟掉分析問題的能力,而不僅僅是調用各種API
和插件,這可能是程序員最寶貴的財富吧。前路坎坷,我輩當不忘初心,願你出走半生,歸來仍是少年!
掃一掃下方小程序碼或搜索Tusi博客
,即刻閱讀最新文章!