需求
鼠标长按圆形按钮,实现环形进度条效果。
一开始做了css的效果,但是css的圆环看起来不圆,所以就用canvas,但是canvas也有缺点,首先是锯齿严重,其次是渲染不方便,需要调用生成N个实例。还是得根据具体使用场景来选择方案了。
效果
css效果
js效果
vue版本效果
CSS代码案例
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>环形进度条</title>
<style>
.wrapper {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.wrapper .center {
width: 46px;
height: 46px;
border-radius: 50%;
font-size: 30px;
text-align: center;
line-height: 40px;
color: #fff;
background: #615fe9;
cursor: pointer;
position: relative;
z-index: 10;
}
.wrapper .center:active + .ring-progress {
display: block;
}
/*
* 环形进度条 *
*/
.ring-progress {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 56px;
height: 56px;
border-radius: 50%;
background: #fff;
margin: auto;
z-index: 9;
display: none;
}
.ring-progress > i {
position: absolute;
top: 0;
left: 0px;
width: 28px;
height: 56px;
background-color: #fff;
border-radius: 28px 0 0 28px;
z-index: 9;
transform-origin: 100% 50%;
transform: rotateZ(0deg);
animation: a3 2s linear;
/*动画只执行一次*/
animation-iteration-count: 1;
/*让动画停留在最后一帧 */
animation-fill-mode: forwards;
}
.ring-progress::after,
.ring-progress::before {
content: "";
position: absolute;
top: 0;
width: 28px;
height: 56px;
background-color: #ff7070;
z-index: 8;
}
.ring-progress::after {
left: 28px;
border-radius: 0 28px 28px 0;
transform-origin: 0 50%;
transform: rotateZ(-180deg) scale(0.96);
animation: a1 1s linear;
/*动画只执行一次*/
animation-iteration-count: 1;
/*让动画停留在最后一帧 */
animation-fill-mode: forwards;
}
.ring-progress::before {
left: 0;
border-radius: 28px 0 0 28px;
transform-origin: 100% 50%;
transform: rotateZ(0deg) scale(0.96);
animation: a2 2s linear;
/*动画只执行一次*/
animation-iteration-count: 1;
/*让动画停留在最后一帧 */
animation-fill-mode: forwards;
/* display: none; */
}
@keyframes a1 {
0% {
transform: rotateZ(-180deg) scale(0.96);
}
100% {
transform: rotateZ(0deg) scale(0.96);
}
}
@keyframes a2 {
0% {
transform: rotateZ(0) scale(0.96);
}
100% {
transform: rotateZ(360deg) scale(0.96);
}
}
@keyframes a3 {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
</style>
</head>
<body>
<div class="wrapper">
<div class="center"></div>
<div class="ring-progress"><i></i></div>
</div>
</body>
</html>
canvas代码案例
由于文字部分自定义需求比较大,就不写死了,有需要可自行定义,可参考 drawTextCB
回调
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>环形进度条</title>
<style>
.wrapper {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.container {
margin-top: 30px;
width: 100px;
height: 100px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="container" id="progress"></div>
</div>
<div class="wrapper">
<button onclick="setProgress(100)">100</button>
<button onclick="setProgress(50)">50</button>
<button onclick="setProgress(25)">25</button>
<button onclick="setProgress(0)">0</button>
</div>
<div class="wrapper">
<div class="container" id="progress2"></div>
</div>
<script>
/**
* 环形进度条
* 功能分析:
* 1、该功能为鼠标按下后出现一个环形进度条,默认进度条起始位置为-90度
* 2、该功能一共有四个层级,从下往上依次为背景层、环形进度条层、中心圆层、居中文字层
* @param {string} containerId - 容器id
* @param {string} bgColor - 背景图层颜色
* @param {number} lineWidth - 进度条宽度
* @param {string} r1Color - 进度条颜色
* @param {string} r2Color - 中心圆颜色
* @param {number} defaultValue - 默认进度位置 (0-100)
* @param {bealoon} animation - 是否开启动画效果
* @param {number} animationTime - 动画时间长度
* @param {function} drawTextCB - 绘制文字的回调
**/
var ops = {
containerId: "",
bgColor: "",
lineWidth: "",
r1Color: "",
r2Color: "",
defaultValue: "",
animation: "",
animationTime: "",
drawTextCB: "",
};
var CircleProgress = function (ops) {
this.containerEl = document.getElementById(ops.containerId);
this.containerEl.innerHTML = `<canvas ></canvas>`;
this.canvasEl = this.containerEl.querySelector("canvas");
this.ctx = this.canvasEl.getContext("2d");
// canvasEl已在此处被重新计算
this.dprInfo = this.getDprInfo();
// 画布真实宽高
this.ctxW = parseFloat(this.canvasEl.style.width);
this.ctxH = parseFloat(this.canvasEl.style.height);
// 是否开启动画
this.animation = ops.animation || true;
// 整体动画时长
this.animationTime = ops.animationTime || 2000;
// 每个百分比的时长
this.stepTime = this.animationTime / 100;
// 当前进度值
this.currentValue = 0;
// 最终结果
this.finalValue = ops.defaultValue || 0;
// 整体背景色
this.bgColor = ops.bgColor || "#ccc";
// 进度条宽度
this.lineWidth = ops.lineWidth || 5;
// 进度条颜色
this.r1Color = ops.r1Color || "#FF7070";
// 中心圆颜色
this.r2Color = ops.r2Color || "#615fe9";
// 提前存储中心圆的路径
this.centerPath = new Path2D();
this.centerPath.arc(
this.ctxW / 2,
this.ctxH / 2,
this.ctxW / 2 - this.lineWidth,
0,
2 * Math.PI
);
// 文字回调函数
this.drawTextCB = ops.drawTextCB || function () {};
// 定时器
this.timer = null;
// 加减类型(true为加,false为减)
this.increase = true;
this.init();
};
CircleProgress.prototype = {
constructor: CircleProgress,
// 初始化
init: function () {
this.setDraw();
},
// 绘制
setDraw: function () {
this.increase = this.finalValue > this.currentValue ? true : false;
if (this.animation) {
this.setAnimationDraw();
} else {
this.currentValue = this.finalValue;
this.drawFrame();
}
},
// 动画进度条
setAnimationDraw: function () {
var that = this;
this.drawFrame();
this.timer = setTimeout(function () {
if (that.increase) {
if (that.currentValue < that.finalValue) {
that.currentValue++;
that.setAnimationDraw();
}
} else {
if (that.currentValue > that.finalValue) {
that.currentValue--;
that.setAnimationDraw();
}
}
this.timer && clearTimeout(this.timer);
}, this.stepTime);
},
/**
* 设置value
* @params {number} value
*/
setValue: function (value) {
this.timer && clearTimeout(this.timer);
this.finalValue = value;
this.setDraw();
},
// 绘制帧
drawFrame: function () {
this.ctx.clearRect(0, 0, this.ctxW, this.ctxH);
this.drawbg();
this.drawCenter();
this.drawProgress();
this.drawText();
},
// 绘制背景环
drawbg: function () {
this.drawCircle({
x: this.ctxW / 2,
y: this.ctxH / 2,
r: this.ctxW / 2 - this.lineWidth / 2,
sAngle: Math.PI * (1 / 180) * -90,
eAngle: Math.PI * (1 / 180) * 270,
color: this.bgColor,
});
},
// 绘制中心圆
drawCenter: function () {
this.drawPathCircle({
path: this.centerPath,
color: this.r2Color,
});
},
// 绘制进度环
drawProgress: function (angle) {
var angle = (this.currentValue / 100) * 360;
this.drawCircle({
x: this.ctxW / 2,
y: this.ctxH / 2,
r: this.ctxW / 2 - this.lineWidth / 2,
sAngle: Math.PI * (1 / 180) * -90,
eAngle: Math.PI * (1 / 180) * (angle - 90),
color: this.r1Color,
});
},
/**
* 绘制圆环
*/
drawCircle: function ({ x, y, r, sAngle, eAngle, color }) {
this.ctx.beginPath();
this.ctx.lineWidth = this.lineWidth;
this.ctx.strokeStyle = color;
this.ctx.arc(x, y, r, sAngle, eAngle);
this.ctx.stroke();
},
/**
* 根据 Path2D 绘制实心圆
*/
drawPathCircle: function ({ path, color }) {
this.ctx.beginPath();
this.ctx.fillStyle = color;
this.ctx.fill(path);
},
// 文本绘制
drawText: function () {
this.drawTextCB && this.drawTextCB();
},
// 解决像素锯齿问题
getDprInfo: function () {
// 解决像素锯齿问题
var cInfo = this.containerEl.getBoundingClientRect();
var dpr = window.devicePixelRatio || 1;
if (dpr) {
this.canvasEl.style.width = cInfo.width + "px";
this.canvasEl.style.height = cInfo.height + "px";
this.canvasEl.width = cInfo.width * dpr;
this.canvasEl.height = cInfo.height * dpr;
this.ctx.scale(dpr, dpr);
}
return { cInfo, dpr };
},
};
/**
* 创建一个进度条按钮类
**/
var CircleProgressBtn = function () {
CircleProgress.call(this, arguments[0]);
// 鼠标按下的时间
this.downTime = 0;
// 是否开启状态
this.isOpen = false;
this.init();
this.addDownHandle();
};
// 继承prototype
(function () {
var Super = function () {};
Super.prototype = CircleProgress.prototype;
CircleProgressBtn.prototype = new Super();
CircleProgressBtn.prototype.constructor = CircleProgressBtn;
})();
// 开始
CircleProgressBtn.prototype.start = function () {
this.step();
};
// 结束
CircleProgressBtn.prototype.end = function () {
this.step();
};
// 清除绘制信息
CircleProgressBtn.prototype.clear = function () {
clearTimeout(this.timer);
this.currentValue = this.isOpen ? 100 : 0;
this.drawFrame();
};
// 添加长按事件
CircleProgressBtn.prototype.addDownHandle = function () {
var that = this;
this.canvasEl.addEventListener("mousedown", function (e) {
var ctxInfo = that.canvasEl.getBoundingClientRect();
that.downTime = new Date().getTime();
if (
that.ctx.isPointInPath(
that.centerPath,
(e.clientX - ctxInfo.left) * that.dprInfo.dpr,
(e.clientY - ctxInfo.top) * that.dprInfo.dpr
)
) {
that.isOpen ? that.end() : that.start();
}
});
document.addEventListener("mouseup", function () {
var t = new Date().getTime() - that.downTime;
if (t < that.animationTime) {
that.clear();
}
});
};
// 步进器
CircleProgressBtn.prototype.step = function () {
var that = this;
if (this.timer) {
clearTimeout(that.timer);
}
this.timer = setTimeout(function () {
if (that.flag) {
return;
}
// 如果是开启状态,长按进度条是减少;否则长按是累加
if (that.isOpen) {
if (that.currentValue > 0) {
that.currentValue--;
that.step();
} else {
that.isOpen = false;
}
} else {
if (that.currentValue < 100) {
that.currentValue++;
that.step();
} else {
that.isOpen = true;
}
}
that.drawFrame();
}, this.stepTime);
};
// 实例1
var p = new CircleProgress({
containerId: "progress",
lineWidth: 20,
defaultValue: 16,
drawTextCB: function () {
// 文字1
this.ctx.font = "20px Verdana";
this.ctx.textBaseline = "middle";
this.ctx.textAlign = "center";
this.ctx.fillStyle = "#fff";
this.ctx.fillText(
this.currentValue,
this.ctxW / 2 - 4,
this.ctxH / 2
);
// 文字2
this.ctx.font = "10px Verdana";
this.ctx.fillText("%", this.ctxW / 2 + 20, this.ctxH / 2);
},
});
function setProgress(n) {
p.setValue(n);
}
// 实例2
var p2 = new CircleProgressBtn({
containerId: "progress2",
lineWidth: 20,
drawTextCB: function () {
// 文字1
this.ctx.font = "20px Verdana";
this.ctx.textBaseline = "middle";
this.ctx.textAlign = "center";
this.ctx.fillStyle = this.isOpen ? "#fff" : "#ccc";
this.ctx.fillText(
this.isOpen ? "NO" : "OFF",
this.ctxW / 2,
this.ctxH / 2
);
},
});
</script>
</body>
</html>
vue版本
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>环形进度条</title>
<style>
.wrapper {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.container {
margin-top: 30px;
width: 100px;
height: 100px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="app">
<div class="wrapper">
<div class="container">
<circle-progress
:default-value="25"
:progress-value="myValue"
:line-width="16"
:btn-mode="true"
v-on:draw-text="handleDrawText"
v-on:start-callback="handleStartCB"
v-on:end-callback="handleEndCB"
></circle-progress>
</div>
</div>
<div class="wrapper"><button @click="handlePlus">+1</button></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
// 注册组件
Vue.component("circle-progress", {
props: {
// 名称
info: {
type: Object,
default: null,
},
// 进度值
progressValue: {
type: [Number, String],
default: 0,
},
// 动画
animation: {
type: Boolean,
default: true,
},
// 动画时间
animationTime: {
type: Number,
default: 2000,
},
// 默认值
defaultValue: {
type: Number,
default: 0,
},
// 背景色
bgColor: {
type: String,
default: "#ccc",
},
// 进度条颜色
r1Color: {
type: String,
default: "#FF7070",
},
// 中心圆颜色
r2Color: {
type: String,
default: "#615fe9",
},
// 进度条粗细
lineWidth: {
type: Number,
default: 5,
},
// 按钮模式
btnMode: {
type: Boolean,
default: false,
},
},
data: function () {
return {
ctx: null,
uid: this.guid(),
// dpr信息
dprInfo: null,
// 画布真实宽高
ctxW: 0,
ctxH: 0,
// 每个百分比的时长
stepTime: this.animationTime / 100,
// 当前进度值
currentValue: 0,
// 最终结果
finalValue: this.defaultValue,
// 提前存储中心圆的路径
centerPath: new Path2D(),
// 定时器
timer: null,
// 进度条类型(true为加,false为减)
increase: true,
// 鼠标按下的时间
downTime: 0,
// 是否开启状态
isOpen: false,
};
},
template: `
<div :id='uid' ref='containerRef' style='width: 100%;height: 100%;'>
<canvas ref='canvasRef' v-on='{ mousedown: handleMouseDown}'></canvas>
</div>
`,
watch: {
progressValue: function (newValue) {
console.log(newValue);
var val = Math.abs(parseInt(newValue));
if (val >= 0 && val <= 100) {
this.finalValue = val;
this.setDraw();
}
},
},
mounted() {
try {
this.init();
} catch (err) {
console.log(err);
}
},
methods: {
// 初始化
init: function () {
this.ctx = this.$refs.canvasRef.getContext("2d");
this.dprInfo = this.getDprInfo();
this.ctx.scale(this.dprInfo.dpr, this.dprInfo.dpr);
this.ctxW = parseFloat(this.$refs.canvasRef.style.width);
this.ctxH = parseFloat(this.$refs.canvasRef.style.height);
this.centerPath.arc(
this.ctxW / 2,
this.ctxH / 2,
this.ctxW / 2 - this.lineWidth,
0,
2 * Math.PI
);
this.setDraw();
document.addEventListener("mouseup", this.handleMouseUp);
},
// 绘制
setDraw: function () {
this.increase = this.finalValue > this.currentValue ? true : false;
if (this.animation) {
this.setAnimationDraw();
} else {
this.currentValue = this.finalValue;
this.drawFrame();
}
},
// 动画进度条
setAnimationDraw: function () {
var that = this;
this.drawFrame();
this.timer = setTimeout(function () {
if (that.increase) {
if (that.currentValue < that.finalValue) {
that.currentValue++;
that.setAnimationDraw();
}
} else {
if (that.currentValue > that.finalValue) {
that.currentValue--;
that.setAnimationDraw();
}
}
}, this.stepTime);
},
/**
* 设置value
* @params {number} value
*/
setValue: function (value) {
// 及时清除定时器,防止定时器叠加
this.timer && clearTimeout(this.timer);
this.finalValue = value;
this.setDraw();
},
// 绘制帧
drawFrame: function () {
this.ctx.clearRect(0, 0, this.ctxW, this.ctxH);
this.drawbg();
this.drawCenter();
this.drawProgress();
this.drawText();
},
// 绘制背景环
drawbg: function () {
this.drawCircle({
x: this.ctxW / 2,
y: this.ctxH / 2,
r: this.ctxW / 2 - this.lineWidth / 2,
sAngle: Math.PI * (1 / 180) * -90,
eAngle: Math.PI * (1 / 180) * 270,
color: this.bgColor,
});
},
// 绘制中心圆
drawCenter: function () {
this.drawPathCircle({
path: this.centerPath,
color: this.r2Color,
});
},
// 绘制进度环
drawProgress: function (angle) {
var angle = (this.currentValue / 100) * 360;
this.drawCircle({
x: this.ctxW / 2,
y: this.ctxH / 2,
r: this.ctxW / 2 - this.lineWidth / 2,
sAngle: Math.PI * (1 / 180) * -90,
eAngle: Math.PI * (1 / 180) * (angle - 90),
color: this.r1Color,
});
},
/**
* 绘制圆环
*/
drawCircle: function ({ x, y, r, sAngle, eAngle, color }) {
// console.log(x, y, r, sAngle, eAngle, color);
this.ctx.beginPath();
this.ctx.lineWidth = this.lineWidth;
this.ctx.strokeStyle = color;
this.ctx.arc(x, y, r, sAngle, eAngle);
this.ctx.stroke();
},
/**
* 根据 Path2D 绘制实心圆
*/
drawPathCircle: function ({ path, color }) {
this.ctx.beginPath();
this.ctx.fillStyle = color;
this.ctx.fill(path);
},
// 文本绘制
drawText: function () {
// this.drawTextCB && this.drawTextCB(this.ctx);
this.$emit("draw-text", this);
},
// 解决像素锯齿问题
getDprInfo: function () {
// 解决像素锯齿问题
var cInfo = this.$refs.containerRef.getBoundingClientRect();
var dpr = window.devicePixelRatio || 1;
if (dpr) {
this.$refs.canvasRef.style.width = cInfo.width + "px";
this.$refs.canvasRef.style.height = cInfo.height + "px";
this.$refs.canvasRef.width = cInfo.width * dpr;
this.$refs.canvasRef.height = cInfo.height * dpr;
}
return { cInfo, dpr };
},
// 生成uid
guid: function () {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
/[xy]/g,
function (c) {
var r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
}
);
},
// 清空帧
clear: function () {
clearTimeout(this.timer);
this.currentValue = this.isOpen ? 100 : 0;
this.drawFrame();
},
// 步进函数
step: function () {
var that = this;
if (this.timer) {
clearTimeout(that.timer);
}
this.timer = setTimeout(function () {
if (that.flag) {
return;
}
// 如果是开启状态,长按进度条是减少;否则长按是累加
if (that.isOpen) {
if (that.currentValue > 0) {
that.currentValue--;
that.step();
} else {
that.isOpen = false;
that.$emit("end-callback", that.info);
}
} else {
if (that.currentValue < 100) {
that.currentValue++;
that.step();
} else {
that.isOpen = true;
that.$emit("start-callback", that.info);
}
}
that.drawFrame();
}, this.stepTime);
},
// 长按事件
handleMouseDown: function (e) {
if (this.btnMode) {
var ctxInfo = this.$refs.canvasRef.getBoundingClientRect();
this.downTime = new Date().getTime();
// 判断长按范围
if (
this.ctx.isPointInPath(
this.centerPath,
(e.clientX - ctxInfo.left) * this.dprInfo.dpr,
(e.clientY - ctxInfo.top) * this.dprInfo.dpr
)
) {
this.step();
}
}
},
handleMouseUp: function () {
var t = new Date().getTime() - this.downTime;
if (t < this.animationTime) {
this.clear();
}
},
},
});
var app = new Vue({
el: "#app",
data: function () {
return {
myValue: 0,
};
},
methods: {
handlePlus: function () {
this.myValue++;
},
// 绘制文字
handleDrawText(_this) {
// 文字1
_this.ctx.font = "20px Verdana";
_this.ctx.textBaseline = "middle";
_this.ctx.textAlign = "center";
_this.ctx.fillStyle = "#fff";
_this.ctx.fillText(
_this.currentValue,
_this.ctxW / 2 - 4,
_this.ctxH / 2
);
// 文字2
_this.ctx.font = "10px Verdana";
_this.ctx.fillText("%", _this.ctxW / 2 + 20, _this.ctxH / 2);
},
// 启动完成回调
handleStartCB: function (info) {
console.log(info);
},
// 关闭完成回调
handleEndCB: function (info) {
console.log(info);
},
},
});
</script>
</body>
</html>