前言
轮播图出现各大网站上-无论是pc还是移动端,尤其是电商网站必然能看见轮播图,它使得用户不用滚动屏幕就能看到更多内容,也常常作为广告位。而作为一个前端工程师,手写轮播图是一个必备的技能。
下图展示了京东,淘宝,腾讯云3个网站的轮播图。最常见的2种轮播图有淡入淡出,无缝轮播。无缝轮播对于用户体验会更好一些。
实现功能
- 实现一个含有5张图片的无缝轮播图。
- 鼠标悬停在轮播图部分时,轮播图停止切换,鼠标离开继续自动切换。
- 通过点击左右2边的按钮,进行轮播图的前一张或后一张的切换。
- 在图片动画未切换完成之前,禁止切换下一张图片。
效果图如下:
何为无缝
无缝轮播图,即是在图片左右切换时,最后一张和第一张相连,也就是当主屏幕显示最后一张图片时,如果用户点击下一张图片时,这时候需要将第一张图片呈现给用户。同理当目前主屏幕上显示第一张图片时,如果用户点击上一张图片时,需要将最后一张图片呈现给用户。
处理办法如下图(序号为当前编号的图片):
初始化轮播图时,我们复制第一张图片与最后一张图片,将复制好的第一张图片放在图片末尾,复制好的最后一张图片放在队列头部。这样当轮播图进行到最后一张时,我们将轮播图位置更改为初始的图片1位置。若我们向左边点击时,遇到图片5时,我们将图片拉到最后一张图片5得位置。这样就不会出现播到最后一张图片后,导致的没图片出现空白的情况。这样就是无缝轮播。
罗列难点
- 滚动到队列末尾时,改为队列第二张图片。
- 用户频繁点击切换图片,之前动画未结束造成的显示错乱。
- 在图片运动结束后,图片没有完全切换完成的情况。
- 图片运动时,等待轮播的计时器未停止。
布局
布局这一块的话,基本没有什么大问题。就直接上代码。
html部分
<div id="wrap">
<div id="box-wrap">
<div class="item">
<img src="https://img10.360buyimg.com//babel/jfs/t1/147387/5/18994/136741/5fdc77afE5f82113e/f4c98e84e67f8fd0.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img11.360buyimg.com//babel/jfs/t1/138375/30/18878/225016/5fdcb77fE3ed18d79/71d924b7f529c6ea.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img12.360buyimg.com//babel/jfs/t1/144721/38/17890/108504/5fd37694E682d34fc/9d1ac8a5d13b94f8.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img10.360buyimg.com//babel/jfs/t1/148646/26/18903/93961/5fdc9321E51f8e513/c4324e7ea048805c.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img14.360buyimg.com//babel/jfs/t1/155501/29/10296/70544/5fdc8f2fE6b2fab26/5423c671aa4e21bf.jpg!q80.webp" alt="">
</div>
</div>
<div class="btn">
<div class="arrow prev" id="left-arrow"><</div>
<div class="arrow next" id="right-arrow">></div>
</div>
</div>
css部分
*{
padding: 0;
margin: 0;
}
#wrap{
position: relative;
width: 780px;
height: 400px;
margin: 50px auto;
border: 1px solid black;
overflow: hidden;
cursor: pointer;
}
#box-wrap{
position: absolute;
left: 0;
top: 0;
display: flex;
}
#box-wrap .item{
width: 780px;
height: 400px;
}
.item img{
width: 100%;
}
.red{
background-color: red;
}
.green{
background-color: green;
}
.black{
background-color: black;
}
.arrow{
position: absolute;
top: 50%;
width: 30px;
height: 30px;
transform: translate(0,-50%);
color: rgb(201, 200, 200);
font-size: 20px;
text-align: center;
line-height: 30px;
background: rgb(0, 0, 0, .3);
}
.arrow:hover{
background: rgb(0, 0, 0, 1);
color: white;
}
.btn{
user-select: none;
}
.btn .prev{
left: 0;
}
.btn .next{
right: 0;
}
功能分析
如上面的代码完成布局之后,效果如下图,接下来我们就需要让图片自动轮播。
布局图
轮播逻辑
DOM加载完成之后通过setInterval、定位,让图片队列盒子#box-wrap在展示图片的盒子中进行移动,即随着时间的变化改变DOM(#box-wrap)的left值。关键代码如下。
let time = null time = setInterval(()=>{ nextRun() },5000)
nextRun = () => {
if (index === itemLength - 1) { // 当图片达到最后一张时,赋值为第一张
index = 1
boxWrap.style.left = -itemWidth*index+"px" // 这里的itemWidth为单张图片宽度
}
++index
move(boxWrap, itemWidth, 15) // 调用运动函数
}move = (el, target, speed) => {
let s = parseInt(boxWrap.style.left) // 当前图片的移动距离
let t = target/speed // 计算时间,总位移距离/单次跑的步长
let s1 = 0 // 当前初始位移let time2 = setInterval(()=>{ s1 += speed el.style.left = -s1 + s + "px" if (s1 + speed > target) { el.style.left = -target + s + "px" clearInterval(time2) } }, t)
}
发现计算步长始终会少那么一点点,最后需要补齐,我们把最后一个参数修改为总时间t,这样时间是会减少到0的。
nextRun = () => {
if (index === itemLength - 1) { // 当图片达到最后一张时,赋值为第一张
index = 1
boxWrap.style.left = -itemWidth*index+"px" // 这里的itemWidth为单张图片宽度
}
++index
move(boxWrap, itemWidth, 1000) // 调用运动函数
}move = (el, target, t) => {
let s = parseInt(boxWrap.style.left)
let s1 = 0
let speed = target/tlet time2 = setInterval(()=>{ s1 += speed el.style.left = -s1 + s + "px" t-- if (t === 0) { clearInterval(time2) } }, 1)
}
这里发现向右的轮播正常了,但是发现时间明明设置的5秒钟的自动轮播,为什么不到5秒就执行了。这里当动画轮播启动时,需要终止自动轮播的计时器,结束以后再重新轮播,下面是关键代码。
run = (fn) => {
time = setInterval(()=>{
fn ? nextRun(fn) : nextRun()
},3000)
}run(run)
move = (el, target, t, fn) => {
let s = parseInt(boxWrap.style.left)
let s1 = 0
let speed = target/tclearInterval(time) let time2 = setInterval(()=>{ s1 += speed el.style.left = -s1 + s + "px" t-- if (t === 0) { if (fn) fn(fn) clearInterval(time2) } }, 1)
}
nextRun = (fn) => {
if (index === itemLength - 1) {
index = 1
boxWrap.style.left = -itemWidth*index+"px"
}++index fn ? move(boxWrap, itemWidth, 500, fn) : move(boxWrap, itemWidth, 500)
}
完整代码
发现一个向右的自动轮播就完成了,向左同理改造move的第三个参数type为轮播方向,接下来的点击切换就简单了,下面是一个无缝轮播的完整代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
padding: 0;
margin: 0;
}
#wrap{
position: relative;
width: 780px;
height: 400px;
margin: 50px auto;
border: 1px solid black;
overflow: hidden;
cursor: pointer;
}
#box-wrap{
position: absolute;
left: 0;
top: 0;
display: flex;
}
#box-wrap .item{
width: 780px;
height: 400px;
}
.item img{
width: 100%;
}
.red{
background-color: red;
}
.green{
background-color: green;
}
.black{
background-color: black;
}
.arrow{
position: absolute;
top: 50%;
width: 30px;
height: 30px;
transform: translate(0,-50%);
color: rgb(201, 200, 200);
font-size: 20px;
text-align: center;
line-height: 30px;
background: rgb(0, 0, 0, .3);
}
.arrow:hover{
background: rgb(0, 0, 0, 1);
color: white;
}
.btn{
user-select: none;
}
.btn .prev{
left: 0;
}
.btn .next{
right: 0;
}
</style>
</head>
<body>
<div id="wrap">
<div id="box-wrap">
<div class="item">
<img src="https://img10.360buyimg.com//babel/jfs/t1/147387/5/18994/136741/5fdc77afE5f82113e/f4c98e84e67f8fd0.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img11.360buyimg.com//babel/jfs/t1/138375/30/18878/225016/5fdcb77fE3ed18d79/71d924b7f529c6ea.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img12.360buyimg.com//babel/jfs/t1/144721/38/17890/108504/5fd37694E682d34fc/9d1ac8a5d13b94f8.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img10.360buyimg.com//babel/jfs/t1/148646/26/18903/93961/5fdc9321E51f8e513/c4324e7ea048805c.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img14.360buyimg.com//babel/jfs/t1/155501/29/10296/70544/5fdc8f2fE6b2fab26/5423c671aa4e21bf.jpg!q80.webp" alt="">
</div>
</div>
<div class="btn">
<div class="arrow prev" id="left-arrow"><</div>
<div class="arrow next" id="right-arrow">></div>
</div>
</div><script> window.onload = () => { (()=>{ let index = 1 let wrap = document.querySelector("#wrap") let boxWrap = document.querySelector("#box-wrap") let imgLen = document.querySelectorAll('#box-wrap div').length let img_first = document.querySelectorAll('#box-wrap div')[0].cloneNode(true) let img_last = document.querySelectorAll('#box-wrap div')[imgLen - 1].cloneNode(true) let leftArrow = document.querySelector("#left-arrow") let rightArrow = document.querySelector("#right-arrow") let itemWidth = document.querySelectorAll("#box-wrap div")[0].offsetWidth boxWrap.appendChild(img_first) boxWrap.insertBefore(img_last, document.querySelectorAll("#box-wrap div")[0]) let itemLength = document.querySelectorAll("#box-wrap div").length boxWrap.style.width = itemWidth * itemLength + "px" boxWrap.style.left = -itemWidth * index + "px" let time = null let isMove = true run = (fn) => { time = setInterval(()=>{ fn ? nextRun(fn) : nextRun() },3000) } run(run) move = (el, target, type, t, fn) => { let s = parseInt(boxWrap.style.left) let s1 = 0 let speed = target/t clearInterval(time) let time2 = setInterval(()=>{ s1 += speed if (type === "left") { el.style.left = -s1 + s + "px" }else{ el.style.left = s1 + s + "px" } t-- if (t === 0) { isMove = true if (fn && time) fn(fn) clearInterval(time2) } }, 1) } wrap.onmouseenter = () => { clearInterval(time) time = null } wrap.onmouseleave = () => { run(run) } nextRun = (fn) => { if (!isMove) return isMove = false if (index === itemLength - 1) { index = 1 boxWrap.style.left = -itemWidth*index+"px" } ++index fn ? move(boxWrap, itemWidth, "left", 500, fn) : move(boxWrap, itemWidth, "left", 500) } prevRun = () => { if (!isMove) return isMove = false if (index === 1) { index = itemLength - 1 boxWrap.style.left = -itemWidth * index + "px" } --index move(boxWrap, itemWidth, "right", 500) } leftArrow.onclick = (e) => { prevRun() } rightArrow.onclick = (e) => { nextRun() } })() } </script>
</body>
</html>