CSS 奇技淫巧Box-shadow实现圆环进度条
一、Box-shadow圆环进度条
实现圆环进度条的方法用很多种,比较容易想到的可能是通过 border
属性实现,在本文将使用 Box-shadow
盒子阴影呈现,一般来说还真的难想到这个方法,说这种方法是一个奇技淫巧也不为过,让我们接着来看。
二、效果预览
<div class="container">
<div class="ring-wrap">
<div class="ring">Hover</div>
</div>
</div>
$borderColor: #ff5d8f;
// 设置阴影
@function setShadow($x: 0, $y: 0, $fuzzy: 0, $spread: 0, $color: $borderColor) {
@return #{$x}px #{$y}px #{$fuzzy}px #{$spread}px $color;
}
.container {
display: flex;
background-color: #6C6C6C;
height: 500px;
}
.ring-wrap {
display: flex;
overflow: hidden;
width: 156px;
height: 156px;
margin: auto;
border-radius: 50%;
.ring {
// 宽高需要预留边框大小
width: 150px;
height: 150px;
line-height: 150px;
margin: auto;
border-radius: 50%;
font-size: 25px;
text-align: center;
color: #fff;
box-shadow: setShadow(75, -75, $color: transparent), setShadow(-75, -75), setShadow(75, -75), setShadow(-75, 75), setShadow(75, 75);
background-color: #2894FF;
cursor: pointer;
&:hover {
animation: ring-border 2s ease-in-out forwards;
}
}
}
@keyframes ring-border {
0% {
box-shadow: setShadow(75, -75, $color: transparent), setShadow(-75, -75), setShadow(75, -75), setShadow(-75, 75), setShadow(75, 75), setShadow($spread: 3, $color: transparent);
}
25% {
box-shadow: setShadow(75, -75, $color: transparent), setShadow(-75, -75), setShadow(0, -160), setShadow(-75, 75), setShadow(75, 75), setShadow($spread: 3, $color: #FFF);
}
50% {
box-shadow: setShadow(75, -75, $color: transparent), setShadow(-160, 0), setShadow(0, -160), setShadow(-75, 75), setShadow(75, 75), setShadow($spread: 3, $color: #FFF);
}
75% {
box-shadow: setShadow(75, -75, $color: #fff), setShadow(-160, 0), setShadow(0, -160), setShadow(0, 160), setShadow(75, 75), setShadow($spread: 3, $color: #FFF);
}
100% {
box-shadow: setShadow(75, -75, $color: #fff), setShadow(-160, 0), setShadow(0, -160), setShadow(0, 160), setShadow(160, 0), setShadow($spread: 3, $color: #FFF);
}
}
三、原理刨析
原理很简单,最重要的是控制阴影按照顺序延时移动,除此之外,还需要一层父元素使用 overflow:hidden
对额外的阴影进行隐藏,父子宽度高不能一致,需要留足阴影填充边框的间隙。
特别注意的是box-shadow
属性使用逗号进行分割多个值,每个值的顺序并不是固定的,其意义简单干脆,就是为元素设置多个不同的阴影。
现在网上很多教程往往在注释或则文中为每个值表明上下左右,为每个值表明顺序,但其实就是不同阴影的xy轴位置不同,本意上是为了标识每个阴影的位置,但好心办坏事造成新手固有思维,不知道的话千万别被误导了。
.ring-wrap {
// ......
.ring {
// ......
box-shadow:
// 左上
setShadow(-75, -75),
// 右上
setShadow(75, -75),
// 左下
setShadow(-75, 75),
// 右下
setShadow(75, 75);
// ......
}
}
使用Box-shadow实现圆环进度条,其实使用四个阴影之间的移动即可完成,网上很多教程是这样,最初的设想也是这样,但最后的效果有点出乎意料。以下是最初的样式。
$borderColor: #ff5d8f;
// 设置阴影
@function setShadow($x: 0, $y: 0, $fuzzy: 0, $spread: 0, $color: $borderColor) {
@return #{$x}px #{$y}px #{$fuzzy}px #{$spread}px $color;
}
.container {
display: flex;
background-color: #6C6C6C;
height: 500px;
}
.ring-wrap {
display: flex;
overflow: hidden;
width: 156px;
height: 156px;
margin: auto;
border-radius: 50%;
.ring {
// 宽高需要预留边框大小
width: 150px;
height: 150px;
line-height: 150px;
margin: auto;
border-radius: 50%;
font-size: 25px;
text-align: center;
color: #fff;
box-shadow: setShadow(-75, -75), setShadow(75, -75), setShadow(-75, 75), setShadow(75, 75);
background-color: #2894FF;
cursor: pointer;
&:hover {
animation: ring-border 5s ease-in-out forwards;
}
}
}
@keyframes ring-border {
0% {
box-shadow: setShadow(-75, -75), setShadow(75, -75), setShadow(-75, 75), setShadow(75, 75), setShadow($spread: 3, $color: transparent);
}
25% {
box-shadow: setShadow(-75, -75), setShadow(0, -160), setShadow(-75, 75), setShadow(75, 75), setShadow($spread: 3, $color: #FFF);
}
50% {
box-shadow: setShadow(-160, 0), setShadow(0, -160), setShadow(-75, 75), setShadow(75, 75), setShadow($spread: 3, $color: #FFF);
}
75% {
box-shadow: setShadow(-160, 0), setShadow(0, -160), setShadow(0, 160), setShadow(75, 75), setShadow($spread: 3, $color: #FFF);
}
100% {
box-shadow: setShadow(-160, 0), setShadow(0, -160), setShadow(0, 160), setShadow(160, 0), setShadow($spread: 3, $color: #FFF);
}
}
为了解决这个问题,通过新增一个阴影进行改进,越前书写的阴影其优先级越高,通过在前书写的阴影覆盖移除的阴影移动,覆盖的阴影要在动画进行到75%或之前完成覆盖以实现效果。
ps:使用码上掘金线上代码编辑器查看效果~
四、实际应用
在悬浮上展示进度条的场景估计很少,一般在与用户交互的场景下使用的会多些。
说了这么多也没有实际用用上,并不知道实际好坏,那咱们简单编写一个轮播图场景进行应用,看看实际效果如何。
<div class="container">
<div class="swipe">
<div class="swipe-img">
<img src="https://w.wallhaven.cc/full/1p/wallhaven-1p398w.jpg" alt class="active">
<img src="https://w.wallhaven.cc/full/7p/wallhaven-7p3we9.png" alt>
<img src="https://w.wallhaven.cc/full/rr/wallhaven-rr2yow.jpg" alt>
</div>
<div class="swipe-btn">
<div class="left-btn">
<b class="btn"> < </b>
</div>
<div class="right-btn">
<b class="btn"> > </b>
</div>
</div>
</div>
</div>
const opts = {
// 控制延迟
interval: 3000,
// 控制方向
direction: "right",
_indexImg: 0,
};
const leftBtn = document.querySelector(".left-btn .btn");
const rightBtn = document.querySelector(".right-btn .btn");
const imgList = document.querySelectorAll(".swipe-img img");
// 获取激活图片索引
function getImgIndex() {
for (let index in imgList) {
const item = imgList[index];
if (Array.from(item.classList).includes("active")) {
return index;
}
}
return 0;
}
/**
* 不同方向处理
* @param {Function} left 左方向处理回调
* @param {Function} right 右方向处理回调
*/
function directionHandle(left, right) {
if (/^left$/i.test(opts.direction)) {
left();
} else {
right();
}
}
function switchSwipe(direction = "auto") {
imgList[opts._indexImg]?.classList?.remove?.("active");
switch (true) {
case /^auto$/i.test(direction):
directionHandle(
() => opts._indexImg--,
() => opts._indexImg++
);
break;
case /^left$/i.test(direction):
opts._indexImg--;
break;
default:
opts._indexImg++;
}
switch (true) {
case opts._indexImg > imgList.length - 1:
opts._indexImg = 0;
break;
case opts._indexImg < 0:
opts._indexImg = imgList.length - 1;
break;
}
imgList[opts._indexImg]?.classList.add("active");
}
function autoPlay() {
opts._indexImg = getImgIndex();
// const
directionHandle(
() => {
leftBtn.style.animationDuration = `${opts.interval / 1000}s`;
leftBtn.classList.add("active");
},
() => {
rightBtn.style.animationDuration = `${opts.interval / 1000}s`;
rightBtn.classList.add("active");
}
);
return setInterval(() => switchSwipe(), opts.interval);
}
function execute() {
// 清除自动播放辅助函数
const clearAuto = (atimer, dtimer) => {
atimer && clearInterval(atimer);
dtimer && clearTimeout(dtimer);
leftBtn.classList.remove("active");
rightBtn.classList.remove("active");
};
let [autoTimer, delayTimer] = [autoPlay(), null];
leftBtn.addEventListener("click", () => {
clearAuto(autoTimer, delayTimer);
switchSwipe("left");
delayTimer = setTimeout(() => {
timer = autoPlay();
}, opts.interval);
});
rightBtn.addEventListener("click", () => {
clearAuto(autoTimer, delayTimer);
switchSwipe("right");
delayTimer = setTimeout(() => {
timer = autoPlay();
}, opts.interval);
});
}
execute();
$borderColor: #ff5d8f;
// 设置阴影
@function setShadow($x: 0, $y: 0, $fuzzy: 0, $spread: 0, $color: $borderColor) {
@return #{$x}px #{$y}px #{$fuzzy}px #{$spread}px $color;
}
.swipe {
position: relative;
width: 100%;
height: 350px;
display: flex;
&-img {
img {
display: none;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
object-fit: cover;
}
.active {
display: block;
}
}
&-btn {
font-size: 30px;
color: #fff;
.left-btn, .right-btn {
display: flex;
position: absolute;
overflow: hidden;
transform: translateY(-50%);
top: 50%;
width: 54px;
height: 54px;
line-height: 45px;
text-align: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, .5);
cursor: pointer;
.active {
animation: swipe-btn ease-in-out infinite forwards;
}
}
.left-btn {
left: 1%;
}
.right-btn {
right: 1%;
}
.btn {
width: 50px;
height: 50px;
margin: auto;
border-radius: 50%;
box-shadow: setShadow(25, -25, $color: transparent), setShadow(-25, -25), setShadow(25, -25), setShadow(-25, 25), setShadow(25, 25);
}
}
}
@keyframes swipe-btn {
0% {
box-shadow: setShadow(25, -25, $color: transparent), setShadow(-25, -25), setShadow(25, -25), setShadow(-25, 25), setShadow(25, 25), setShadow($spread: 2, $color: transparent);
}
25% {
box-shadow: setShadow(25, -25, $color: transparent), setShadow(-25, -25), setShadow(0, -60), setShadow(-25, 25), setShadow(25, 25), setShadow($spread: 2, $color: #FFF);
}
50% {
box-shadow: setShadow(25, -25, $color: transparent), setShadow(-60, 0), setShadow(0, -60), setShadow(-25, 25), setShadow(25, 25), setShadow($spread: 2, $color: #FFF);
}
75% {
box-shadow: setShadow(25, -25, $color: #fff), setShadow(-60, 0), setShadow(0, -60), setShadow(0, 60), setShadow(25, 25), setShadow($spread: 2, $color: #FFF);
}
100% {
box-shadow: setShadow(25, -25, $color: #fff), setShadow(-60, 0), setShadow(0, -60), setShadow(0, 60), setShadow(60, 0), setShadow($spread: 2, $color: #FFF);
}
}
ps:使用码上掘金线上代码编辑器查看效果~
五、总结
其实使用Box-shadow进行实现的关键点在于控制阴影按照顺序延时移动,移动的越快速度则越快,反则越慢。为解决最后一阴影移动便宜的问题,需要新增一个优先级高的阴影提前进行覆盖,当然嫌麻烦的话可以不用。
优缺点:
- 因为是移动四个不同的阴影来控制进度,在阴影的切换处很明显会有顿挫感,对于需要平滑进度条的场景来说不太适用,但对于需要顿挫感的场景来说又很适用,可谓是一把双刃剑,关键要看在哪里用。
- 使用阴影控制圆环进度条,这个方法是比较难想到的,实现起来还需要一层父元素,编写起来需要一定的熟练度。
- 兼容性方面会强一些,只要浏览器支持animation动画,大多数可以实现。
另外值得一提的是,在一些 UI组件库中,环形进度条一般是已经被封装好的,直接拿来用即可,以下图element ui为例。
六、参考资料💘
- 官方手册:
- 网络文献:
- 相关连接: