前言
最近要开发一个五一问卷调查活动,这个功能用到了轮播图,翻阅了一下项目文件,看到项目中别的地方轮播图用的是react-id-swiper
工具包, 本着已有功能就要充分复用的宗旨思想,依葫芦画瓢,把别处的react-id-swiper
轮播代码功能搬了过来,改了轮播内容区域,发现运行不正常,第一页跳转到第二页的时候,需要点击两次才能跳转成功。查阅react-id-swiper组件库的文档,没有找到合适的解决方案。后面发现是第一次跳转的时候,dom元素还未创建,所以调用swiper的跳转方法执行无效所致。这件事让我对轮播图的实现原理产生了兴趣,决定抽空研究一下,现在我们进入正题。
效果展示
这是最终的效果图,可以看到无论是自动轮播,还是手动向前,向后翻,翻到头都可以做到平滑的过渡,分页器的切换也是OK的,将这个轮播组件应用于生产实践,一点问题也没有。接下来说说它的实现原理。
轮播图基础原理
- 首先创建一个容器元素,用于放置轮播的内容。设置容器的宽高,以及 overflow 属性为 hidden,以隐藏超出部分的轮播项。
- 如果轮播方向为水平方向,要设置所有的轮播项显示在一行,不能换行。每个轮播项的宽度等于容器宽度,方便控制轮播顺序。
- 通过设置容器元素的
transform:translateX(移动距离)
属性控制显示哪一个轮播项。
先用纯CSS实现轮播效果
轮播图的html结构如下所示:slider-box
区域是轮播内容区,slider-spot
区域是分页区域,控制轮播图的页码切换。
html
<div class="slider-container">
<ul class="slider-box">
<li class="slider-item"><img src="./imgs/1.jpg" alt="" /></li>
<li class="slider-item"><img src="./imgs/2.webp" alt="" /></li>
<li class="slider-item"><img src="./imgs/3.webp" alt="" /></li>
</ul>
<div class="slider-spot">
<input type="radio" name="pager" class="spot-item" id="page1" checked />
<input type="radio" name="pager" class="spot-item" id="page2" />
<input type="radio" name="pager" class="spot-item" id="page3" />
</div>
</div>
通过设置容器slider-container
的overflow: hidden;
属性,隐藏掉不需要显示的轮播项。设置轮播父元素slider-box
的display
属性为flex
让轮播元素展示为一行,这时你会发现,每个轮播元素被压扁了。小问题,只需给轮播元素slider-item
添加flex: 0 0 100%;
,不让其扩展和压缩即可。借助radio单选按钮组中每个单选按钮的checked
状态,就可知道要切换到哪一个轮播项。接着用has
伪类反向选择轮播项父元素slider-container
,计算好X轴方向的偏移量,偏移到指定位置,就实现了轮播图的切换效果。
css
ul {
margin: 0;
padding: 0;
list-style: none;
}
.slider-container {
position: relative;
width: 375px;
height: 250px;
margin: 0 auto;
overflow: hidden;
.slider-box {
height: 100%;
display: flex;
.slider-item {
height: 100%;
flex: 0 0 100%;
img {
width: 100%;
height: 100%;
}
}
}
}
.slider-container:has(#page1:checked) > .slider-box,
.slider-container:has(#page2:checked) > .slider-box,
.slider-container:has(#page3:checked) > .slider-box {
transition: 1s ease-in-out;
}
.slider-container:has(#page1:checked) > .slider-box {
transform: translateX(0);
}
.slider-container:has(#page2:checked) > .slider-box {
transform: translateX(-375px);
}
.slider-container:has(#page3:checked) > .slider-box {
transform: translateX(-750px);
}
.slider-spot {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
bottom: 10px;
width: 100%;
height: 15px;
.spot-item {
width: 15px;
height: 15px;
& + & {
margin-left: 10px;
}
}
}
效果演示:
再用JS实现轮播图
上面通过用纯CSS实现轮播效果,是为了理解轮播图的原理。纯CSS实现的轮播图,在轮播项数量不确定的条件下,每一项的偏移位置不好计算。还有无法实现左右滑动切换轮播项。现在我们根据轮播图的原理,用js实现一下轮播图。
把轮播图修改成支持参数配置(配置参数参见下面),动态生成轮播项。其它的参数都比较好理解,有一个参数要说明一下。配置参数中有一项是容器id,这个参数是考虑到一个页面中可能会有多个轮播图,为了防止相互之间产生串扰。页面中引入了一个slider.js的文件,这个文件是轮播图功能的核心功能实现,接下来会重点讲解。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>轮播图</title>
<style>
.slider-container {
position: relative;
width: 375px;
height: 250px;
margin: 0 auto;
overflow: hidden;
}
</style>
</head>
<body>
<div id="my-slider" class="slider-container"></div>
</body>
<script src="./slider.js"></script>
<script>
let imgList = [
{
imgPath: "./imgs/1.jpg",
jumpUrl: "#",
},
{
imgPath: "./imgs/2.webp",
jumpUrl: "#",
},
{
imgPath: "./imgs/3.webp",
jumpUrl: "#",
},
];
new Slider({
// 轮播图容器id
sliderId: "my-slider",
// 轮播图列表
imgList: imgList,
// 是否自动播放
autoplay: true,
// 轮播项切换过渡动画时长
aniTIme: 1000,
// 自动播放轮播项切换时间间隔,单位毫秒
intervalTime: 1000,
}).init();
</script>
</html>
笔者按照自顶向下的思路,讲解一下自动轮播的实现。首先定义Slider
轮播构造函数,接收外部传入的参数,检查关键的参数是否设置,没有设置的话会在控制台抛出错误。
js
/**
* config:
* sliderId 轮播图容器id
* imgList 轮播图列表
* autoplay 是否自动播放
* aniTIme 动画时长
* intervalTime 自动播放轮播图切换时间
*/
function Slider(config) {
if (!config.sliderId) {
throw new Error("轮播容器id不能为空");
}}
this.imgList = config.imgList || [];
if (this.imgList.length === 0) {
throw new Error("轮播列表不能为空");
}
// 轮播图片父容器
this.sliderBoxDom=null;
// 向前翻按钮
this.prevBtnDom=null;
// 向后翻按钮
this.nextBtnDom=null;
// 轮播分页容器
this.sliderSpotDom=null;
// 自动播放定时器
this.timer = null;
// 轮播外层容器
this.sliderContainerDom = document.getElementById(config.sliderId);
// 轮播图片外层容器每次移动的偏移量
this.moveWidth = this.sliderContainerDom.offsetWidth;
this.aniTIme = config.aniTIme || 1000;
this.intervalTime = config.intervalTime + this.aniTIme || 2000;
// 当前播放的l轮播图序号
this.playIndex = 0;
// 自动播放设置
this.autoplay = config.autoplay;
}
初始化函数中做了二件事情,第一动态创建与轮播图相关的dom元素,第二判断执行自动播放逻辑。
js
Slider.prototype = {
init() {
this.createSliderDom();
if (this.autoplay) {
this.timer = setInterval(this.toNext.bind(this, this.aniTIme), this.intervalTime);
}
},
}
createSliderDom
方法内部的逻辑是在轮播外层容器my-slider
下面创建轮播父容器slider-box
及轮播项内容slider-item
,以及分页器slider-spot
及元素节点slider-item
, 设置轮播父容器slider-box
的初始偏移。
js
// 动态创建轮播图节点内容
createSliderDom() {
// 创建轮播图片容器
this.sliderBoxDom = document.createElement("ul");
this.sliderBoxDom.className = "slider-box";
// 添加轮播图片
this.sliderBoxDom.innerHTML = this.imgList
.map((item, index) => {
let dom = [
`<li class="slider-item">`,
`<a href="${item.jumpUrl}"><img src="${item.imgPath}" alt=""></a>`,
"</li>",
];
return dom.join("");
})
.join("");
// 初始偏移位置
this.sliderBoxDom.style.transform = `translateX(0)`;
// 创建切换轮播图的分页器按钮容器
this.sliderSpotDom = document.createElement("ul");
this.sliderSpotDom.className = "slider-spot";
// 分页器
this.sliderSpotDom.innerHTML = this.imgList
.map((item, index) => {
return `<li class="spot-item ${index === 0 ? "active" : ""}" data-index=${index}></li>`;
})
.join("");
this.sliderContainerDom.appendChild(this.sliderBoxDom);
this.sliderContainerDom.appendChild(this.sliderSpotDom);
},
执行自动轮播的功能是依靠toNext
方法完成的, 如果只有一张图片的话,无需设置轮播容器的偏移了。第一幅图偏移量为0,从第二幅图开始,按照轮播容器宽度的倍数进行偏移,当轮播到最后一张时,把轮播容器的偏移量重置0,开始新一轮的循环播放。此外toNext
方法中还调用了setActiveSpot
方法,setActiveSpot
方法用于设置分页页码高亮,轮播到哪一项,给这一项添加高亮样式类active
。
js
// 向后翻
toNext(aniTIme) {
// 轮播图只有一项的话,无需设置偏移
if (this.imgList.length === 1) return;
this.playIndex++;
if (this.playIndex === this.imgList.length) {
this.playIndex = 0;
}
this.sliderBoxDom.style.transition = `${aniTIme / 1000}s ease-in-out`;
this.sliderBoxDom.style.transform = `translateX(${-this.playIndex * this.moveWidth}px)`;
this.setActiveSpot();
},
// 设置当前分页按钮为高亮选中状态
setActiveSpot() {
this.sliderSpotDom.childNodes.forEach((item, index) => {
if (index === Math.abs(this.playIndex)) {
item.classList.add("active");
} else {
item.classList.remove("active");
}
});
},
你会发现实现的这个自动轮播功能,有个很大的问题,播放到最后一张图片重新开始下一轮播放时,过渡很不自然,有些突兀。而我们经常使用的一些轮播组件,最后一张和第一张的过渡很平滑,原因出在哪里,别人是怎么做到的? 带着这个疑问,我查阅了一些资料,发现里面有点名堂。
无缝轮播的实现原理
左移实现
上面的自动轮播,要想实现左移无缝轮播的效果,需要更改一下页面结构。在最后一幅图片后面,要多放置一张图片,把首张图片追加到最后一幅图的后面。
html
<div class="slider-container">
<ul class="slider-box">
<li class="slider-item"><img src="./imgs/1.jpg" alt="" /></li>
<li class="slider-item"><img src="./imgs/2.webp" alt="" /></li>
<li class="slider-item"><img src="./imgs/3.webp" alt="" /></li>
<!-- 追加的首图-->
<li class="slider-item"><img src="./imgs/1.jpg" alt="" /></li>
</ul>
</div>
原理我画图解释一下:假设轮播容器的宽度是375px, 只有三张图片,红框框线是轮播可视区域,其它图片都是隐藏的。 图中的数字表示第几张图片。
- 第一次轮播容器的水平偏移量为0,展示第一幅图
- 第二次轮播容器的水平偏移量为-375px,展示第二幅图
- 第三次轮播容器的水平偏移量为-750px,展示第三幅图
- 第四次是实现左移无缝轮播的关键,第四次轮播容器的水平偏移量为-1125px,展示末尾的第一幅图,这时要偷梁换柱,根据轮播动画播放时长,设置一个延时器(这个延时器的时间是小于轮播图切换的时长的),在延时器里把轮播容器水平偏移量重置为0,并且要取消轮播过渡动画。
- 第五次轮播容器的水平偏移量为0,展示第一幅图,开始第二轮的周而复始。
涉及的代码变动有两处,第一处是在createSliderDom
方法中,创建轮播元素列表时,在末尾追加一张首图。
js
// 动态创建轮播图节点内容
createSliderDom() {
// ...
// 创建轮播图片容器--在末尾追加首图
let realImgList = [...this.imgList, this.imgList[0]];
// 添加轮播图片
this.sliderBoxDom.innerHTML = realImgList
.map((item, index) => {
let dom = [
`<li class="slider-item">`,
`<a href="${item.jumpUrl}"><img src="${item.imgPath}" alt=""></a>`,
"</li>",
];
return dom.join("");
})
.join("");
// ...
},
第二处改动是在toNext
方法中,判断播放到最后一幅图的时候,重置轮播容器水平偏移量计算变量值,并根据轮播过渡动画的时长,设置一个延时器,在延时器里,重置轮播容器水平偏移量为轮播列表中的第一幅图,并取消过渡动画,实现无缝平滑轮播。
js
// 向后翻
toNext(aniTIme) {
if (this.imgList.length === 1) return;
this.playIndex++;
this.sliderBoxDom.style.transition = `${aniTIme / 1000}s ease-in-out`;
this.sliderBoxDom.style.transform = `translateX(${-this.playIndex * this.moveWidth}px)`;
if (this.playIndex === this.imgList.length) {
this.playIndex = 0;
setTimeout(() => {
this.sliderBoxDom.style.transitionProperty = "none";
this.sliderBoxDom.style.transform = `translateX(0)`;
}, aniTIme);
}
this.setActiveSpot();
},
经过改造之后,现在的自动轮播效果,播放到最后一张向首张过渡时,是不是丝滑自然了很多。
右移实现
如果自动轮播的方向是向右移动的话,要实现无缝轮播,需将最后一幅图放置到列表首图前面,页面结构为:
html
<div class="slider-container">
<ul class="slider-box">
<!-- 追加的尾图-->
<li class="slider-item"><img src="./imgs/3.jpg" alt="" /></li>
<li class="slider-item"><img src="./imgs/1.jpg" alt="" /></li>
<li class="slider-item"><img src="./imgs/2.webp" alt="" /></li>
<li class="slider-item"><img src="./imgs/3.webp" alt="" /></li>
</ul>
</div>
假设轮播容器的宽度为375px,共三张图,轮播顺序为:
- 第一次轮播容器的水平偏移量为-375px,展示轮播列表第二幅图
- 第二次轮播容器的水平偏移量为 0,展示轮播列表第一幅图,第二次是实现右移无缝轮播的关键,这时要加一个延时器,在延时器里,把轮播容器的偏移量设置为-1125px,展示轮播列表最后一幅图,并取消动画过渡效果。
- 第三次轮播容器的水平偏移量为-1125px,展示轮播列表最后一幅图
- 第四次轮播容器的水平偏移量为-750px,展示轮播列表第三幅图
- 第五次轮播容器的水平偏移量为-375px,展示轮播列第二幅图,开始第二轮的周而复始。
引起的代码变化:第一是在createSliderDom
方法中,创建轮播元素列表时,在头部追加一张尾图。此外要将轮播容器的初始偏移位置设置为-375px
,让其展示第一幅轮播图。
js
// 动态创建轮播图节点内容
createSliderDom() {
// ...
// 创建轮播图片容器
let realImgList = [this.imgList[this.imgList.length - 1], ...this.imgList];
// 添加轮播图片
this.sliderBoxDom.innerHTML = realImgList
.map((item, index) => {
let dom = [
`<li class="slider-item">`,
`<a href="${item.jumpUrl}"><img src="${item.imgPath}" alt=""></a>`,
"</li>",
];
return dom.join("");
})
.join("");
this.playIndex=1;
// 初始偏移位置
this.sliderBoxDom.style.transform = `translateX(${-this.moveWidth}px)`;
// ...
},
第二处改动新增toPrev
方法,判断播放到轮播列表第一幅的时候,重置轮播容器水平偏移量计算变量值,并根据轮播过渡动画的时长,设置一个延时器,在延时器里,重置轮播容器水平偏移量为轮播列表中的最后一幅图,并取消过渡动画,实现右移无缝平滑轮播。
js
// 向前翻
toPrev(aniTIme) {
if (this.imgList.length === 1) return;
this.sliderBoxDom.style.transition = `${aniTIme / 1000}s ease-in-out`;
this.sliderBoxDom.style.transform = `translateX(${-this.playIndex * this.moveWidth}px)`;
if (this.playIndex === 0) {
this.playIndex = this.imgList.length - 1;
setTimeout(() => {
this.sliderBoxDom.style.transitionProperty = "none";
this.sliderBoxDom.style.transform = `translateX(${-this.imgList.length * this.moveWidth}px)`;
}, aniTIme);
} else {
this.playIndex--;
}
this.setActiveSpot();
},
右移无缝轮播效果展示:可以看到第一幅图和最后一幅图的过渡效果自然流畅。
最后
文章介绍了自动轮播的核心实现原理,至于轮播图的其它附加功能如手动分页,向前,向后翻页,以及移动端的手势滑动分页切换功能,也已实现,限于篇幅,不再赘述。亲手实现了轮播图之后,才发现无缝轮播这一块,需要特殊处理一下,才能实现平滑切换,有点名堂。果然应了那句纸上得来终觉浅,绝知此事要躬行。如果有时间有精力的话,还是建议大家把项目中常用的功能组件,自己亲自实现一遍,你一定会收获很多别人不曾知道的细节知识。本文的完整代码已经上传至码云,如果你对轮播图感兴趣的话,可以点击这里下载学习。