半环形进度条在起点活动中的应用
前言
相信看到此文的小伙伴们,平时工作中多多少少应该会接触过进度条方面的需求,比如直角进度条、圆角进度条、圆环进度条等,大家可能也有多种方法比如利用 CSS
canvas
svg
去实现这些需求并予以加上动画。而本次要和大家分享的就是半环形进度条如何用 canvas
和 svg
去实现,以及如何在实现的基础上去增强用户的体验。
先来看视觉稿和最终实现
为了方便演示,本文中的案例均使用单文件的 vue
+html
来做代码实例演示
Canvas 实现步骤
一、准备工作
我们首先要做一些 canvas
的基本准备工作。
先来了解一下 canvas
的画圆 arc
方法的用法和绘制角度原理。
参数 | 描述 |
---|---|
x | 圆的中心的 x 坐标。 |
y | 圆的中心的 y 坐标。 |
r | 圆的半径。 |
sAngle | 起始角,以弧度计(弧的圆形的三点钟位置是 0 度) |
eAngle | 结束角,以弧度计。 |
counterclockwise | 可选。逆时针还是顺时针。false = 顺时针,true = 逆时针 |
然后我们定义一些初始变量
data() {
return {
canvas: null, // canvas 实例对象
cWidth: 750, // 预设宽度
cHeight: 750, // 预设高度
progress: 50, // 假设从接口获取的进度目前是 50
}
},
接着我们在 methods
中添加一个名为 initCircleProgress
的方法,定义一些绘制所需的变量以及角度等等单位
// initCircleProgress 方法体中的代码
let radius = 124 // 外环半径
let thickness = 12 // 圆环厚度
let innerRadius = radius - thickness // 内环半径
let startAngle = -180 // 开始⾓度
let endAngle = 0 // 结束⾓度
let x = 0 // 圆⼼x坐标
let y = 0 // 圆⼼y坐标
对 canvas
做好初始化的准备
// html 结构
<canvas id="circleProgress"></canvas>
// initCircleProgress 方法体中的代码
this.canvas = document.getElementById('circleProgress')
let ctx = this.canvas.getContext('2d')
this.canvas.style.width = this.cWidth + 'px'
this.canvas.style.height = this.cHeight + 'px'
this.canvas.height = this.cHeight
this.canvas.width = this.cWidth
// 将绘图原点移到画布中央
ctx.translate(document.body.clientWidth / 2, document.body.clientWidth / 2)
ctx.fillStyle = '#FFF' // 初始填充颜⾊
二、绘制半圆环
我们借鉴了 canvas
绘制饼图的方法,先封装一个角度转弧度的小函数,以便之后调用
// ⾓度转弧度
function angle2Radian(angle) {
return (angle * Math.PI) / 180
}
接下来我们先开始绘制外环,为了今后动画调用方便。我们再封装一个 renderRing
函数,它接受 2 个参数,分别是开始角度 startAngle
和结束角度 endAngle
renderRing(startAngle, endAngle)
function renderRing(startAngle, endAngle) {
ctx.beginPath()
// 绘制外环
ctx.arc(0, 0, radius, angle2Radian(startAngle), angle2Radian(endAngle))
}
以上代码就可以形成一个这样的半圆饼图
/* 绘制内环 依然参考 canvas 饼图、环形图的一些技巧,通过逆时针绘制内圆形成进轨道
* 这里的 innerRadius 内圆半径在上面定义过,所以是 radius - thickness = 12
*/
ctx.arc(0, 0, innerRadius, angle2Radian(endAngle), angle2Radian(startAngle), true) // 从-180 到 0
目前得到的半圆环,左侧和右侧是平的,而视觉稿进度条的起点和终点都是圆弧形的,所以接下来一点比较重要,需要你努力回忆起你初中时学过的三角函数知识,利用 cos
和 sin
算出 x,y 坐标后画小圆,关于 canvas
如何计算圆的坐标可以参考此文
x = Math.cos(Math.PI * 2 / 360 * 度数) * r
y = Math.sin(Math.PI * 2 / 360 * 度数) * r
现在是半圆,所以只需把 360
改成 180
即可。我们再封一个 calcRingPoint
小函数,用来计算左右两侧小圆的坐标,随后在终点处画上一个小圆,左边的同理。
// 计算圆环上点的坐标
function calcRingPoint(x, y, radius, angle) {
let res = {}
res.x = x + radius * Math.cos((angle * Math.PI) / 180)
res.y = y + radius * Math.sin((angle * Math.PI) / 180)
return res
}
// 接着绘制
function renderRing(startAngle, endAngle) {
...
// 计算外环与内环终点连接处的中⼼坐标
let oneCtrlPoint = calcRingPoint(
x,
y,
innerRadius + thickness / 2,
endAngle
)
// 绘制外环与内终点连接处的圆环
ctx.arc(
oneCtrlPoint.x,
oneCtrlPoint.y,
thickness / 2,
angle2Radian(-90),
angle2Radian(270) // 可任意调整,只要和原来平的轨道合并成一个圆即可
)
// 计算外环与内环起点连接处的中⼼坐标
let twoCtrlPoint = calcRingPoint(
x,
y,
innerRadius + thickness / 2,
startAngle
)
// 绘制外环与内环起点连接处的圆环
ctx.arc(
twoCtrlPoint.x,
twoCtrlPoint.y,
thickness / 2,
angle2Radian(-90),
angle2Radian(270)
)
ctx.fill()
}
经过以上步骤,我们就得到这样一个。。。有点问题的半圆环???,别急,我们需要调整一下,把绘制内环放到中间去执行,让起点处的绘制在最上层。
这下就正常了~
三、优化,提高还原度
以上的结果似乎和视觉稿还差了一点,视觉稿要比半圆更长一点,所以我们需要整体扩大一下起始角度和结束角度,一调整后我们发现原本摆正的圆被旋转了,这时候就需要再用 rotate
方法将其进行视觉摆正,如下方右图。
let startAngle = -65 // 开始⾓度
let endAngle = 155 // 结束⾓度
ctx.rotate(angle2Radian(225)) // 将画布旋转225度
最后,我们要在轨道中不停的绘制新的小圆(这里其实是整个画布都在不停重绘),模拟进度条增涨的动画效果。
// 进度条颜⾊
let progress = ctx.createLinearGradient(0, 0, 500, 0)
progress.addColorStop(0, '#1075EB')
progress.addColorStop(1, '#FFF')
ctx.fillStyle = progress
// 开始绘画
let tempAngle = startAngle
let total = 100 // 总进度
let percent = this.progress / total // 百分⽐
let twoEndAngle = percent * 220 + startAngle // 半圆原本是180,加长后是220
let step = (twoEndAngle - startAngle) / 100 // 设置步长速度
function animLoop() {
if (tempAngle < twoEndAngle) {
tempAngle += step
renderRing(startAngle, tempAngle)
window.requestAnimationFrame(animLoop)
}
}
animLoop()
大功告成!什么?有明显的锯齿?不慌,我们再来做一个小优化。
小知识点: canvas
毕竟只是绘图接口,他就像 photoshop
一样,越放大越容易出现锯齿,曲线则更为明显。知道这个原理就好办多了,我们首先需要将图像的宽高倍率放大。
let devicePixelRatio = 4 // 定义一个设备像素倍率变量
this.canvas.height = this.cHeight * devicePixelRatio
this.canvas.width = this.cWidth * devicePixelRatio
// 再缩放抗锯齿
ctx.scale(devicePixelRatio, devicePixelRatio)
经过调整后,平滑和高清度改善许多,我们和视觉稿对比已经是高度还原了。
svg 实现步骤
一、先取一个 svg 整圆,加上圆环
介于 svg
矢量图的特性,它不会存在像 canvas
那样出现有锯齿的情况,我们只需运用好 circle
标签的两个关键属性 stroke-dasharray
和 stroke-linecap
。
首先,我们可以先从网上借一个 svg
圆环图过来,做一些微调,得到下列的样子
<svg width="440" height="440" viewbox="0 0 440 440">
<circle cx="220" cy="220" r="140" stroke-width="16" stroke="#FFF" fill="none"></circle>
<circle cx="220" cy="220" r="140" stroke-width="16" stroke="#00A5E0" fill="none" stroke-dasharray="260 879"></circle>
</svg>
我们再来回忆一下初中数学:c=2πr
圆的周长 = 圆周率 * 2 * 半径
, 以上圆就是 3.14 * 2* 140 = 879
在这里 stroke-dasharray
表示虚线长为 260,间距为 879,以此我们可以实现以上大约四分之一进度的圆环进度条,而由于间距879(此圆的周长),所以下一段虚线我们并看不见。
二、改造 svg 整圆
我们需要的是半圆,用以上 svg
开始改造。半径为 140 的圆,周长是 140 * 2 * 3.14 = 879,一半的圆周长就是 430 左右。两头的小圆则不需要像 canvas
那样复杂,只需加上属性 stroke-linecap="round"
即可。
<svg width="440" height="440" viewBox="0 -100 440 440" class="svg-out out">
<circle cx="180" cy="220" r="140"
stroke-width="16"
stroke="#FFF"
fill="none"
stroke-dasharray="430"
stroke-linecap="round"
></circle>
</svg>
这样就得到了一个倾斜半圆环,我们再把周长加长一些,旋转后找到一个平衡点,调整到想要的效果
.out {
width: 366px;
transform: rotate(-200deg);
}
此时 svg
中的 stroke-dasharray
目前是 535。我们再把内圆调整成想要的颜色,调整好属性,先形成一个静态的 svg
效果
<svg width="440" height="440" viewBox="0 -100 440 440" class="svg-out out">
<!-- 外环 -->
<circle cx="180" cy="220" r="140"
stroke-width="16"
stroke="#FFF"
fill="none"
stroke-dasharray="535"
stroke-linecap="round"
></circle>
<!-- 内环 -->
<circle class="inner" cx="180" cy="220" r="140" stroke-width="16" stroke="#1075EB" fill="none" stroke-dasharray="0 879" stroke-linecap="round"></circle>
</svg>
三、svg 实现动态轨道进度条
由于 svg
实现方法和 canvas
完全不一样,从 0-100 的进度值,是按照圆的周长来计算的,所以我们要把实际的进度条数值做减半处理,来显示 svg
目前所在圆环内的当前进度值。
mounted() {
this.calcSvgProgress(this.progress) // progress: 50
},
methods: {
calcSvgProgress(progress, delay = 500) {
// 整圆c=2πr,半圆则是c=πr r=180是半圆,我们目前比半圆多一点点,所以取 170
let percent = progress / 100, perimeter = Math.PI * 170
setTimeout(() => {
document.querySelector('.inner').setAttribute('stroke-dasharray', perimeter * percent + " 879");
}, delay)
}
别忘了用 css
加上动画过渡
.inner {
transition: stroke-dasharray 1s;
}
经过以上代码,我们动态的把 stroke-dasharray
从 0 879,变成了 267 879,这样就能让进度条从 0 滑动到一半,到此为止,我们用 svg
实现了与 canvas
一模一样的效果,并且,他还是矢量的!
数据更新时的体验优化与差异
按照我们活动业务中的场景,用户可能在完成任务后随时能看到进度条的增涨变化或升级变化。
在这方面,如果你不做一些定制取优化体验的话,canvas
和 svg
的表现就完全不同了。
首先来看 canvas
由于canvas
每次变化都是重绘,所以他每次都会从 0 开始绘制到最终值。
再来看 svg
由于 svg
本身就是矢量图形,所以在它发生变化时,只是进行了变形或者位移,动画也比较连贯。
假设用户升级后,发现看到的进度条由多变少,这就显得比较奇怪了。
不过,好在 svg
以及 svg
内部的标签也都属于 dom
元素,所以我们是可以用 js
对它们进行一些操控的。我们用简单的一段代码就能模拟出升级的效果
svgLevelUp() {
let circle = document.querySelector('.inner');
let perimeter = Math.PI * 170
circle.setAttribute('stroke-dasharray', perimeter + " 879");
setTimeout(() => {
circle.style.display = 'none'
circle.setAttribute('stroke-dasharray', '0 879')
setTimeout(() => {
circle.style.display = 'block'
this.calcSvgProgress(25, 100)
}, 10)
}, 1000)
},
原理很简单:先把 stroke-dasharray
改成满值,做个延时 → 内环元素暂时 display:none
,同时改回空(0,879)再延时 → 重新让内环 display:block
,此时再拉接口获取最新 progress
进度值,来达到一个让用户看到升级后,进度又涨到 25% 进度的效果。
总结
canvas
实现过程较为复杂,但可以自由调整细节,比较容易应付多变的需求,整个进度条 0-100 值和实际数值相匹配。
svg
实现过程简单,代码量也比较少,按照 stroke-dasharray
的特性,若需求较为复杂时,可能在视觉还原度上会有一些难度,并有一定的局限性,需要自行再添加方法优化体验,整个进度条 0-100 的值是按照整圆来计算的,若半圆则需要减半做换算处理。
没有最好的方案,只有最适合的方案。以上就是本次分享所带来的所有内容了,希望能给屏幕前的小伙伴们开拓一些思路,感谢阅读。
转载自:https://juejin.cn/post/7186917605213470776